Skip to main content

nucleus/security/
caps_policy.rs

1//! Capability policy: external TOML-based capability configuration.
2//!
3//! Allows operators to define capability bounding/ambient/effective/inheritable
4//! sets in a standalone TOML file, separate from Nix service definitions.
5//!
6//! # Example
7//!
8//! ```toml
9//! # Drop everything (default). Empty keep lists = deny all.
10//! [bounding]
11//! keep = []
12//!
13//! [ambient]
14//! keep = []
15//!
16//! # Or keep specific capabilities:
17//! # [bounding]
18//! # keep = ["NET_BIND_SERVICE"]
19//! ```
20
21use crate::error::{NucleusError, Result};
22use crate::security::{CapabilityManager, CapabilitySets};
23use caps::Capability;
24use serde::Deserialize;
25use tracing::info;
26
27/// Parsed capability policy from a TOML file.
28#[derive(Debug, Clone, Deserialize)]
29pub struct CapsPolicy {
30    /// Bounding set configuration. Empty keep = drop all from bounding.
31    #[serde(default)]
32    pub bounding: CapSetPolicy,
33
34    /// Ambient set configuration.
35    #[serde(default)]
36    pub ambient: CapSetPolicy,
37
38    /// Effective set configuration.
39    #[serde(default)]
40    pub effective: CapSetPolicy,
41
42    /// Inheritable set configuration.
43    #[serde(default)]
44    pub inheritable: CapSetPolicy,
45}
46
47/// Policy for a single capability set.
48#[derive(Debug, Clone, Deserialize, Default)]
49pub struct CapSetPolicy {
50    /// Capabilities to keep. Empty list = drop all.
51    /// Names use Linux format without CAP_ prefix: e.g. "NET_BIND_SERVICE".
52    #[serde(default)]
53    pub keep: Vec<String>,
54}
55
56/// Capabilities that are too dangerous for container workloads.
57/// These must be explicitly rejected in production mode.
58const 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    /// Validate that the policy does not retain dangerous capabilities
80    /// in production mode.
81    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    /// Apply this policy using the given CapabilityManager.
108    ///
109    /// If all sets are empty, delegates to `drop_all()`.
110    /// Otherwise, applies each set explicitly.
111    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    /// Resolve all keep lists into a deduplicated set of Capability values.
150    #[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
182/// Parse a capability name string to a `caps::Capability` enum variant.
183///
184/// Accepts names with or without the `CAP_` prefix:
185/// - `"NET_BIND_SERVICE"` or `"CAP_NET_BIND_SERVICE"` both work.
186fn 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}