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