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];
73
74impl CapsPolicy {
75    /// Validate that the policy does not retain dangerous capabilities
76    /// in production mode.
77    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    /// Apply this policy using the given CapabilityManager.
104    ///
105    /// If all sets are empty, delegates to `drop_all()`.
106    /// Otherwise, applies each set explicitly.
107    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    /// Resolve all keep lists into a deduplicated set of Capability values.
146    #[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
178/// Parse a capability name string to a `caps::Capability` enum variant.
179///
180/// Accepts names with or without the `CAP_` prefix:
181/// - `"NET_BIND_SERVICE"` or `"CAP_NET_BIND_SERVICE"` both work.
182fn 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}