1use crate::error::{NucleusError, Result};
22use crate::security::{CapabilityManager, CapabilitySets};
23use caps::Capability;
24use serde::Deserialize;
25use tracing::info;
26
27#[derive(Debug, Clone, Deserialize)]
29pub struct CapsPolicy {
30 #[serde(default)]
32 pub bounding: CapSetPolicy,
33
34 #[serde(default)]
36 pub ambient: CapSetPolicy,
37
38 #[serde(default)]
40 pub effective: CapSetPolicy,
41
42 #[serde(default)]
44 pub inheritable: CapSetPolicy,
45}
46
47#[derive(Debug, Clone, Deserialize, Default)]
49pub struct CapSetPolicy {
50 #[serde(default)]
53 pub keep: Vec<String>,
54}
55
56const DANGEROUS_CAPABILITIES: &[Capability] = &[
59 Capability::CAP_SYS_ADMIN,
60 Capability::CAP_SYS_MODULE,
61 Capability::CAP_SYS_RAWIO,
62 Capability::CAP_SYS_PTRACE,
63 Capability::CAP_DAC_OVERRIDE,
64 Capability::CAP_DAC_READ_SEARCH,
65 Capability::CAP_SYS_BOOT,
66 Capability::CAP_MAC_ADMIN,
67 Capability::CAP_MAC_OVERRIDE,
68 Capability::CAP_SYS_PACCT,
69 Capability::CAP_LINUX_IMMUTABLE,
70 Capability::CAP_BPF,
71 Capability::CAP_PERFMON,
72 Capability::CAP_NET_RAW,
73 Capability::CAP_SETUID,
74 Capability::CAP_SETGID,
75 Capability::CAP_FOWNER,
76];
77
78impl CapsPolicy {
79 pub fn validate_production(&self) -> Result<()> {
82 let sets = self.resolve_sets()?;
83 let all_kept: Vec<&Capability> = sets
84 .bounding
85 .iter()
86 .chain(&sets.permitted)
87 .chain(&sets.effective)
88 .chain(&sets.inheritable)
89 .chain(&sets.ambient)
90 .collect();
91 let mut rejected = Vec::new();
92 for &dangerous in DANGEROUS_CAPABILITIES {
93 if all_kept.contains(&&dangerous) {
94 rejected.push(format!("{:?}", dangerous));
95 }
96 }
97 if !rejected.is_empty() {
98 return Err(NucleusError::ConfigError(format!(
99 "Capability policy retains dangerous capabilities in production mode: [{}]. \
100 These must be removed for production workloads.",
101 rejected.join(", ")
102 )));
103 }
104 Ok(())
105 }
106
107 pub fn apply(&self, mgr: &mut CapabilityManager) -> Result<()> {
112 let sets = self.resolve_sets()?;
113
114 if sets.bounding.is_empty()
115 && sets.permitted.is_empty()
116 && sets.effective.is_empty()
117 && sets.inheritable.is_empty()
118 && sets.ambient.is_empty()
119 {
120 info!("Capability policy: drop all");
121 mgr.drop_all()
122 } else {
123 info!("Capability policy: applying explicit sets {:?}", sets);
124 mgr.apply_sets(&sets)
125 }
126 }
127
128 fn resolve_sets(&self) -> Result<CapabilitySets> {
129 let bounding = resolve_cap_list(&self.bounding.keep)?;
130 let effective = resolve_cap_list(&self.effective.keep)?;
131 let ambient = resolve_cap_list(&self.ambient.keep)?;
132 let mut inheritable = resolve_cap_list(&self.inheritable.keep)?;
133 extend_unique(&mut inheritable, &ambient);
134
135 let mut permitted = Vec::new();
136 extend_unique(&mut permitted, &effective);
137 extend_unique(&mut permitted, &inheritable);
138 extend_unique(&mut permitted, &ambient);
139
140 Ok(CapabilitySets {
141 bounding,
142 permitted,
143 effective,
144 inheritable,
145 ambient,
146 })
147 }
148
149 #[cfg(test)]
151 fn resolve_keep_set(&self) -> Result<Vec<Capability>> {
152 let sets = self.resolve_sets()?;
153 let mut caps = Vec::new();
154 extend_unique(&mut caps, &sets.bounding);
155 extend_unique(&mut caps, &sets.permitted);
156 extend_unique(&mut caps, &sets.effective);
157 extend_unique(&mut caps, &sets.inheritable);
158 extend_unique(&mut caps, &sets.ambient);
159 Ok(caps)
160 }
161}
162
163fn resolve_cap_list(names: &[String]) -> Result<Vec<Capability>> {
164 let mut caps = Vec::new();
165 for name in names {
166 let cap = parse_capability_name(name)?;
167 if !caps.contains(&cap) {
168 caps.push(cap);
169 }
170 }
171 Ok(caps)
172}
173
174fn extend_unique(dst: &mut Vec<Capability>, src: &[Capability]) {
175 for &cap in src {
176 if !dst.contains(&cap) {
177 dst.push(cap);
178 }
179 }
180}
181
182fn parse_capability_name(name: &str) -> Result<Capability> {
187 let normalized = name.strip_prefix("CAP_").unwrap_or(name);
188 match normalized {
189 "CHOWN" => Ok(Capability::CAP_CHOWN),
190 "DAC_OVERRIDE" => Ok(Capability::CAP_DAC_OVERRIDE),
191 "DAC_READ_SEARCH" => Ok(Capability::CAP_DAC_READ_SEARCH),
192 "FOWNER" => Ok(Capability::CAP_FOWNER),
193 "FSETID" => Ok(Capability::CAP_FSETID),
194 "KILL" => Ok(Capability::CAP_KILL),
195 "SETGID" => Ok(Capability::CAP_SETGID),
196 "SETUID" => Ok(Capability::CAP_SETUID),
197 "SETPCAP" => Ok(Capability::CAP_SETPCAP),
198 "LINUX_IMMUTABLE" => Ok(Capability::CAP_LINUX_IMMUTABLE),
199 "NET_BIND_SERVICE" => Ok(Capability::CAP_NET_BIND_SERVICE),
200 "NET_BROADCAST" => Ok(Capability::CAP_NET_BROADCAST),
201 "NET_ADMIN" => Ok(Capability::CAP_NET_ADMIN),
202 "NET_RAW" => Ok(Capability::CAP_NET_RAW),
203 "IPC_LOCK" => Ok(Capability::CAP_IPC_LOCK),
204 "IPC_OWNER" => Ok(Capability::CAP_IPC_OWNER),
205 "SYS_MODULE" => Ok(Capability::CAP_SYS_MODULE),
206 "SYS_RAWIO" => Ok(Capability::CAP_SYS_RAWIO),
207 "SYS_CHROOT" => Ok(Capability::CAP_SYS_CHROOT),
208 "SYS_PTRACE" => Ok(Capability::CAP_SYS_PTRACE),
209 "SYS_PACCT" => Ok(Capability::CAP_SYS_PACCT),
210 "SYS_ADMIN" => Ok(Capability::CAP_SYS_ADMIN),
211 "SYS_BOOT" => Ok(Capability::CAP_SYS_BOOT),
212 "SYS_NICE" => Ok(Capability::CAP_SYS_NICE),
213 "SYS_RESOURCE" => Ok(Capability::CAP_SYS_RESOURCE),
214 "SYS_TIME" => Ok(Capability::CAP_SYS_TIME),
215 "SYS_TTY_CONFIG" => Ok(Capability::CAP_SYS_TTY_CONFIG),
216 "MKNOD" => Ok(Capability::CAP_MKNOD),
217 "LEASE" => Ok(Capability::CAP_LEASE),
218 "AUDIT_WRITE" => Ok(Capability::CAP_AUDIT_WRITE),
219 "AUDIT_CONTROL" => Ok(Capability::CAP_AUDIT_CONTROL),
220 "SETFCAP" => Ok(Capability::CAP_SETFCAP),
221 "MAC_OVERRIDE" => Ok(Capability::CAP_MAC_OVERRIDE),
222 "MAC_ADMIN" => Ok(Capability::CAP_MAC_ADMIN),
223 "SYSLOG" => Ok(Capability::CAP_SYSLOG),
224 "WAKE_ALARM" => Ok(Capability::CAP_WAKE_ALARM),
225 "BLOCK_SUSPEND" => Ok(Capability::CAP_BLOCK_SUSPEND),
226 "AUDIT_READ" => Ok(Capability::CAP_AUDIT_READ),
227 "PERFMON" => Ok(Capability::CAP_PERFMON),
228 "BPF" => Ok(Capability::CAP_BPF),
229 "CHECKPOINT_RESTORE" => Ok(Capability::CAP_CHECKPOINT_RESTORE),
230 _ => Err(NucleusError::ConfigError(format!(
231 "Unknown capability: '{}'. Use Linux names like NET_BIND_SERVICE.",
232 name
233 ))),
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 #[test]
242 fn test_parse_drop_all_policy() {
243 let toml = r#"
244[bounding]
245keep = []
246
247[ambient]
248keep = []
249"#;
250 let policy: CapsPolicy = toml::from_str(toml).unwrap();
251 assert!(policy.bounding.keep.is_empty());
252 assert!(policy.resolve_keep_set().unwrap().is_empty());
253 }
254
255 #[test]
256 fn test_parse_keep_some_policy() {
257 let toml = r#"
258[bounding]
259keep = ["NET_BIND_SERVICE", "CHOWN"]
260"#;
261 let policy: CapsPolicy = toml::from_str(toml).unwrap();
262 let keep = policy.resolve_keep_set().unwrap();
263 assert_eq!(keep.len(), 2);
264 assert!(keep.contains(&Capability::CAP_NET_BIND_SERVICE));
265 assert!(keep.contains(&Capability::CAP_CHOWN));
266 }
267
268 #[test]
269 fn test_parse_cap_prefix() {
270 assert_eq!(
271 parse_capability_name("CAP_NET_RAW").unwrap(),
272 Capability::CAP_NET_RAW
273 );
274 assert_eq!(
275 parse_capability_name("NET_RAW").unwrap(),
276 Capability::CAP_NET_RAW
277 );
278 }
279
280 #[test]
281 fn test_unknown_capability_error() {
282 assert!(parse_capability_name("DOES_NOT_EXIST").is_err());
283 }
284
285 #[test]
286 fn test_default_policy_is_drop_all() {
287 let toml = "";
288 let policy: CapsPolicy = toml::from_str(toml).unwrap();
289 assert!(policy.resolve_keep_set().unwrap().is_empty());
290 }
291
292 #[test]
293 fn test_dedup_across_sets() {
294 let toml = r#"
295[bounding]
296keep = ["CHOWN"]
297
298[effective]
299keep = ["CHOWN"]
300"#;
301 let policy: CapsPolicy = toml::from_str(toml).unwrap();
302 let keep = policy.resolve_keep_set().unwrap();
303 assert_eq!(keep.len(), 1);
304 }
305
306 #[test]
307 fn test_resolve_sets_preserves_set_specificity() {
308 let toml = r#"
309[bounding]
310keep = ["NET_BIND_SERVICE"]
311
312[effective]
313keep = ["CHOWN"]
314
315[ambient]
316keep = ["NET_BIND_SERVICE"]
317"#;
318 let policy: CapsPolicy = toml::from_str(toml).unwrap();
319 let resolved = policy.resolve_sets().unwrap();
320
321 assert_eq!(resolved.bounding, vec![Capability::CAP_NET_BIND_SERVICE]);
322 assert_eq!(resolved.effective, vec![Capability::CAP_CHOWN]);
323 assert_eq!(resolved.ambient, vec![Capability::CAP_NET_BIND_SERVICE]);
324 assert_eq!(resolved.inheritable, vec![Capability::CAP_NET_BIND_SERVICE]);
325 assert_eq!(
326 resolved.permitted,
327 vec![Capability::CAP_CHOWN, Capability::CAP_NET_BIND_SERVICE]
328 );
329 }
330
331 #[test]
332 fn test_ambient_caps_promote_into_inheritable_and_permitted() {
333 let toml = r#"
334[ambient]
335keep = ["NET_RAW"]
336"#;
337 let policy: CapsPolicy = toml::from_str(toml).unwrap();
338 let resolved = policy.resolve_sets().unwrap();
339
340 assert_eq!(resolved.ambient, vec![Capability::CAP_NET_RAW]);
341 assert_eq!(resolved.inheritable, vec![Capability::CAP_NET_RAW]);
342 assert_eq!(resolved.permitted, vec![Capability::CAP_NET_RAW]);
343 }
344
345 #[test]
346 fn test_validate_production_rejects_newly_classified_dangerous_caps() {
347 let toml = r#"
348[bounding]
349keep = ["NET_RAW", "SETUID", "SETGID", "FOWNER"]
350"#;
351 let policy: CapsPolicy = toml::from_str(toml).unwrap();
352
353 let err = policy.validate_production().unwrap_err();
354 let msg = err.to_string();
355 assert!(msg.contains("CAP_NET_RAW"));
356 assert!(msg.contains("CAP_SETUID"));
357 assert!(msg.contains("CAP_SETGID"));
358 assert!(msg.contains("CAP_FOWNER"));
359 }
360}