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
56impl CapsPolicy {
57    /// Apply this policy using the given CapabilityManager.
58    ///
59    /// If all sets are empty, delegates to `drop_all()`.
60    /// Otherwise, applies each set explicitly.
61    pub fn apply(&self, mgr: &mut CapabilityManager) -> Result<()> {
62        let sets = self.resolve_sets()?;
63
64        if sets.bounding.is_empty()
65            && sets.permitted.is_empty()
66            && sets.effective.is_empty()
67            && sets.inheritable.is_empty()
68            && sets.ambient.is_empty()
69        {
70            info!("Capability policy: drop all");
71            mgr.drop_all()
72        } else {
73            info!("Capability policy: applying explicit sets {:?}", sets);
74            mgr.apply_sets(&sets)
75        }
76    }
77
78    fn resolve_sets(&self) -> Result<CapabilitySets> {
79        let bounding = resolve_cap_list(&self.bounding.keep)?;
80        let effective = resolve_cap_list(&self.effective.keep)?;
81        let ambient = resolve_cap_list(&self.ambient.keep)?;
82        let mut inheritable = resolve_cap_list(&self.inheritable.keep)?;
83        extend_unique(&mut inheritable, &ambient);
84
85        let mut permitted = Vec::new();
86        extend_unique(&mut permitted, &effective);
87        extend_unique(&mut permitted, &inheritable);
88        extend_unique(&mut permitted, &ambient);
89
90        Ok(CapabilitySets {
91            bounding,
92            permitted,
93            effective,
94            inheritable,
95            ambient,
96        })
97    }
98
99    /// Resolve all keep lists into a deduplicated set of Capability values.
100    #[cfg(test)]
101    fn resolve_keep_set(&self) -> Result<Vec<Capability>> {
102        let sets = self.resolve_sets()?;
103        let mut caps = Vec::new();
104        extend_unique(&mut caps, &sets.bounding);
105        extend_unique(&mut caps, &sets.permitted);
106        extend_unique(&mut caps, &sets.effective);
107        extend_unique(&mut caps, &sets.inheritable);
108        extend_unique(&mut caps, &sets.ambient);
109        Ok(caps)
110    }
111}
112
113fn resolve_cap_list(names: &[String]) -> Result<Vec<Capability>> {
114    let mut caps = Vec::new();
115    for name in names {
116        let cap = parse_capability_name(name)?;
117        if !caps.contains(&cap) {
118            caps.push(cap);
119        }
120    }
121    Ok(caps)
122}
123
124fn extend_unique(dst: &mut Vec<Capability>, src: &[Capability]) {
125    for &cap in src {
126        if !dst.contains(&cap) {
127            dst.push(cap);
128        }
129    }
130}
131
132/// Parse a capability name string to a `caps::Capability` enum variant.
133///
134/// Accepts names with or without the `CAP_` prefix:
135/// - `"NET_BIND_SERVICE"` or `"CAP_NET_BIND_SERVICE"` both work.
136fn parse_capability_name(name: &str) -> Result<Capability> {
137    let normalized = name.strip_prefix("CAP_").unwrap_or(name);
138    match normalized {
139        "CHOWN" => Ok(Capability::CAP_CHOWN),
140        "DAC_OVERRIDE" => Ok(Capability::CAP_DAC_OVERRIDE),
141        "DAC_READ_SEARCH" => Ok(Capability::CAP_DAC_READ_SEARCH),
142        "FOWNER" => Ok(Capability::CAP_FOWNER),
143        "FSETID" => Ok(Capability::CAP_FSETID),
144        "KILL" => Ok(Capability::CAP_KILL),
145        "SETGID" => Ok(Capability::CAP_SETGID),
146        "SETUID" => Ok(Capability::CAP_SETUID),
147        "SETPCAP" => Ok(Capability::CAP_SETPCAP),
148        "LINUX_IMMUTABLE" => Ok(Capability::CAP_LINUX_IMMUTABLE),
149        "NET_BIND_SERVICE" => Ok(Capability::CAP_NET_BIND_SERVICE),
150        "NET_BROADCAST" => Ok(Capability::CAP_NET_BROADCAST),
151        "NET_ADMIN" => Ok(Capability::CAP_NET_ADMIN),
152        "NET_RAW" => Ok(Capability::CAP_NET_RAW),
153        "IPC_LOCK" => Ok(Capability::CAP_IPC_LOCK),
154        "IPC_OWNER" => Ok(Capability::CAP_IPC_OWNER),
155        "SYS_MODULE" => Ok(Capability::CAP_SYS_MODULE),
156        "SYS_RAWIO" => Ok(Capability::CAP_SYS_RAWIO),
157        "SYS_CHROOT" => Ok(Capability::CAP_SYS_CHROOT),
158        "SYS_PTRACE" => Ok(Capability::CAP_SYS_PTRACE),
159        "SYS_PACCT" => Ok(Capability::CAP_SYS_PACCT),
160        "SYS_ADMIN" => Ok(Capability::CAP_SYS_ADMIN),
161        "SYS_BOOT" => Ok(Capability::CAP_SYS_BOOT),
162        "SYS_NICE" => Ok(Capability::CAP_SYS_NICE),
163        "SYS_RESOURCE" => Ok(Capability::CAP_SYS_RESOURCE),
164        "SYS_TIME" => Ok(Capability::CAP_SYS_TIME),
165        "SYS_TTY_CONFIG" => Ok(Capability::CAP_SYS_TTY_CONFIG),
166        "MKNOD" => Ok(Capability::CAP_MKNOD),
167        "LEASE" => Ok(Capability::CAP_LEASE),
168        "AUDIT_WRITE" => Ok(Capability::CAP_AUDIT_WRITE),
169        "AUDIT_CONTROL" => Ok(Capability::CAP_AUDIT_CONTROL),
170        "SETFCAP" => Ok(Capability::CAP_SETFCAP),
171        "MAC_OVERRIDE" => Ok(Capability::CAP_MAC_OVERRIDE),
172        "MAC_ADMIN" => Ok(Capability::CAP_MAC_ADMIN),
173        "SYSLOG" => Ok(Capability::CAP_SYSLOG),
174        "WAKE_ALARM" => Ok(Capability::CAP_WAKE_ALARM),
175        "BLOCK_SUSPEND" => Ok(Capability::CAP_BLOCK_SUSPEND),
176        "AUDIT_READ" => Ok(Capability::CAP_AUDIT_READ),
177        "PERFMON" => Ok(Capability::CAP_PERFMON),
178        "BPF" => Ok(Capability::CAP_BPF),
179        "CHECKPOINT_RESTORE" => Ok(Capability::CAP_CHECKPOINT_RESTORE),
180        _ => Err(NucleusError::ConfigError(format!(
181            "Unknown capability: '{}'. Use Linux names like NET_BIND_SERVICE.",
182            name
183        ))),
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[test]
192    fn test_parse_drop_all_policy() {
193        let toml = r#"
194[bounding]
195keep = []
196
197[ambient]
198keep = []
199"#;
200        let policy: CapsPolicy = toml::from_str(toml).unwrap();
201        assert!(policy.bounding.keep.is_empty());
202        assert!(policy.resolve_keep_set().unwrap().is_empty());
203    }
204
205    #[test]
206    fn test_parse_keep_some_policy() {
207        let toml = r#"
208[bounding]
209keep = ["NET_BIND_SERVICE", "CHOWN"]
210"#;
211        let policy: CapsPolicy = toml::from_str(toml).unwrap();
212        let keep = policy.resolve_keep_set().unwrap();
213        assert_eq!(keep.len(), 2);
214        assert!(keep.contains(&Capability::CAP_NET_BIND_SERVICE));
215        assert!(keep.contains(&Capability::CAP_CHOWN));
216    }
217
218    #[test]
219    fn test_parse_cap_prefix() {
220        assert_eq!(
221            parse_capability_name("CAP_NET_RAW").unwrap(),
222            Capability::CAP_NET_RAW
223        );
224        assert_eq!(
225            parse_capability_name("NET_RAW").unwrap(),
226            Capability::CAP_NET_RAW
227        );
228    }
229
230    #[test]
231    fn test_unknown_capability_error() {
232        assert!(parse_capability_name("DOES_NOT_EXIST").is_err());
233    }
234
235    #[test]
236    fn test_default_policy_is_drop_all() {
237        let toml = "";
238        let policy: CapsPolicy = toml::from_str(toml).unwrap();
239        assert!(policy.resolve_keep_set().unwrap().is_empty());
240    }
241
242    #[test]
243    fn test_dedup_across_sets() {
244        let toml = r#"
245[bounding]
246keep = ["CHOWN"]
247
248[effective]
249keep = ["CHOWN"]
250"#;
251        let policy: CapsPolicy = toml::from_str(toml).unwrap();
252        let keep = policy.resolve_keep_set().unwrap();
253        assert_eq!(keep.len(), 1);
254    }
255
256    #[test]
257    fn test_resolve_sets_preserves_set_specificity() {
258        let toml = r#"
259[bounding]
260keep = ["NET_BIND_SERVICE"]
261
262[effective]
263keep = ["CHOWN"]
264
265[ambient]
266keep = ["NET_BIND_SERVICE"]
267"#;
268        let policy: CapsPolicy = toml::from_str(toml).unwrap();
269        let resolved = policy.resolve_sets().unwrap();
270
271        assert_eq!(resolved.bounding, vec![Capability::CAP_NET_BIND_SERVICE]);
272        assert_eq!(resolved.effective, vec![Capability::CAP_CHOWN]);
273        assert_eq!(resolved.ambient, vec![Capability::CAP_NET_BIND_SERVICE]);
274        assert_eq!(resolved.inheritable, vec![Capability::CAP_NET_BIND_SERVICE]);
275        assert_eq!(
276            resolved.permitted,
277            vec![Capability::CAP_CHOWN, Capability::CAP_NET_BIND_SERVICE]
278        );
279    }
280
281    #[test]
282    fn test_ambient_caps_promote_into_inheritable_and_permitted() {
283        let toml = r#"
284[ambient]
285keep = ["NET_RAW"]
286"#;
287        let policy: CapsPolicy = toml::from_str(toml).unwrap();
288        let resolved = policy.resolve_sets().unwrap();
289
290        assert_eq!(resolved.ambient, vec![Capability::CAP_NET_RAW]);
291        assert_eq!(resolved.inheritable, vec![Capability::CAP_NET_RAW]);
292        assert_eq!(resolved.permitted, vec![Capability::CAP_NET_RAW]);
293    }
294}