Skip to main content

nucleus/security/
capabilities.rs

1use crate::error::{NucleusError, Result};
2use caps::{CapSet, Capability, CapsHashSet};
3use tracing::{debug, info};
4
5/// Security context that tracks capability state
6pub struct CapabilityManager {
7    dropped: bool,
8}
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct CapabilitySets {
12    pub bounding: Vec<Capability>,
13    pub permitted: Vec<Capability>,
14    pub effective: Vec<Capability>,
15    pub inheritable: Vec<Capability>,
16    pub ambient: Vec<Capability>,
17}
18
19impl CapabilityManager {
20    pub fn new() -> Self {
21        Self { dropped: false }
22    }
23
24    /// Drop all capabilities
25    ///
26    /// This implements the transition: Privileged -> CapabilitiesDropped
27    /// in the security state machine (Nucleus_Security_SecurityEnforcement.tla)
28    pub fn drop_all(&mut self) -> Result<()> {
29        if self.dropped {
30            debug!("Capabilities already dropped, skipping");
31            return Ok(());
32        }
33
34        info!("Dropping all capabilities");
35
36        // Clear all capability sets
37        caps::clear(None, CapSet::Permitted).map_err(|e| {
38            NucleusError::CapabilityError(format!("Failed to clear permitted caps: {}", e))
39        })?;
40
41        caps::clear(None, CapSet::Effective).map_err(|e| {
42            NucleusError::CapabilityError(format!("Failed to clear effective caps: {}", e))
43        })?;
44
45        caps::clear(None, CapSet::Inheritable).map_err(|e| {
46            NucleusError::CapabilityError(format!("Failed to clear inheritable caps: {}", e))
47        })?;
48
49        caps::clear(None, CapSet::Ambient).map_err(|e| {
50            NucleusError::CapabilityError(format!("Failed to clear ambient caps: {}", e))
51        })?;
52
53        // Clear bounding set: prevents regaining capabilities through exec of setuid binaries
54        for cap in caps::all() {
55            if let Err(e) = caps::drop(None, CapSet::Bounding, cap) {
56                // Some capabilities may not be in the bounding set; log and continue
57                debug!(
58                    "Failed to drop bounding cap {:?}: {} (may not be present)",
59                    cap, e
60                );
61            }
62        }
63
64        // M4: Verify the bounding set is actually empty after the drop loop
65        let bounding = caps::read(None, CapSet::Bounding).map_err(|e| {
66            NucleusError::CapabilityError(format!("Failed to read bounding set after drop: {}", e))
67        })?;
68        if !bounding.is_empty() {
69            let leaked: Vec<String> = bounding.iter().map(|c| format!("{:?}", c)).collect();
70            return Err(NucleusError::CapabilityError(format!(
71                "Bounding set still contains capabilities after drop_all: [{}]",
72                leaked.join(", ")
73            )));
74        }
75
76        self.dropped = true;
77        info!("Successfully dropped all capabilities (including bounding set)");
78
79        Ok(())
80    }
81
82    /// Drop all capabilities except the specified ones
83    ///
84    /// For most use cases, we drop ALL capabilities. This method is provided
85    /// for special cases where specific capabilities are needed.
86    pub fn drop_except(&mut self, keep: &[Capability]) -> Result<()> {
87        if self.dropped {
88            debug!("Capabilities already dropped, skipping");
89            return Ok(());
90        }
91
92        info!("Dropping capabilities except: {:?}", keep);
93
94        // Get all capabilities
95        let all_caps = caps::all();
96
97        // Drop each capability that's not in the keep list
98        for cap in all_caps {
99            if !keep.contains(&cap) {
100                caps::drop(None, CapSet::Permitted, cap).map_err(|e| {
101                    NucleusError::CapabilityError(format!("Failed to drop {cap:?}: {e}"))
102                })?;
103
104                caps::drop(None, CapSet::Effective, cap).map_err(|e| {
105                    NucleusError::CapabilityError(format!("Failed to drop {cap:?}: {e}"))
106                })?;
107
108                caps::drop(None, CapSet::Inheritable, cap).map_err(|e| {
109                    NucleusError::CapabilityError(format!("Failed to drop {cap:?}: {e}"))
110                })?;
111
112                if let Err(e) = caps::drop(None, CapSet::Bounding, cap) {
113                    debug!(
114                        "Failed to drop bounding cap {:?}: {} (may not be present)",
115                        cap, e
116                    );
117                }
118            }
119        }
120
121        // Always clear ambient capabilities
122        caps::clear(None, CapSet::Ambient).map_err(|e| {
123            NucleusError::CapabilityError(format!("Failed to clear ambient caps: {}", e))
124        })?;
125
126        self.dropped = true;
127        info!("Successfully dropped capabilities");
128
129        Ok(())
130    }
131
132    /// Apply explicit capability sets.
133    ///
134    /// Bounding is handled as a drop-only upper bound; the remaining sets are
135    /// set exactly to the provided values.
136    pub fn apply_sets(&mut self, sets: &CapabilitySets) -> Result<()> {
137        if self.dropped {
138            debug!("Capabilities already dropped, skipping");
139            return Ok(());
140        }
141
142        info!("Applying explicit capability sets");
143
144        for cap in caps::all() {
145            if !sets.bounding.contains(&cap) {
146                if let Err(e) = caps::drop(None, CapSet::Bounding, cap) {
147                    debug!(
148                        "Failed to drop bounding cap {:?}: {} (may not be present)",
149                        cap, e
150                    );
151                }
152            }
153        }
154
155        // M5: Set Permitted first, then Effective immediately after to avoid a
156        // window where the old effective set exceeds the new permitted set.
157        caps::set(None, CapSet::Permitted, &to_caps_hash_set(&sets.permitted)).map_err(|e| {
158            NucleusError::CapabilityError(format!("Failed to set permitted caps: {}", e))
159        })?;
160        caps::set(None, CapSet::Effective, &to_caps_hash_set(&sets.effective)).map_err(|e| {
161            NucleusError::CapabilityError(format!("Failed to set effective caps: {}", e))
162        })?;
163        caps::set(
164            None,
165            CapSet::Inheritable,
166            &to_caps_hash_set(&sets.inheritable),
167        )
168        .map_err(|e| {
169            NucleusError::CapabilityError(format!("Failed to set inheritable caps: {}", e))
170        })?;
171        caps::set(None, CapSet::Ambient, &to_caps_hash_set(&sets.ambient)).map_err(|e| {
172            NucleusError::CapabilityError(format!("Failed to set ambient caps: {}", e))
173        })?;
174
175        self.dropped = true;
176        info!("Successfully applied capability sets");
177        Ok(())
178    }
179
180    /// Check if capabilities have been dropped
181    pub fn is_dropped(&self) -> bool {
182        self.dropped
183    }
184
185    /// Verify that namespace-creating capabilities are actually absent from
186    /// the effective set. This is a runtime guard for the clone3 seccomp
187    /// invariant: clone3 cannot be argument-filtered at the BPF level, so
188    /// we rely on CAP_SYS_ADMIN (et al.) being dropped to prevent namespace
189    /// creation. If the check fails in production mode, it returns an error;
190    /// otherwise it emits a warning.
191    pub fn verify_no_namespace_caps(production: bool) -> Result<()> {
192        use caps::Capability;
193        let ns_caps = [
194            Capability::CAP_SYS_ADMIN,
195            Capability::CAP_NET_ADMIN,
196            Capability::CAP_SYS_PTRACE,
197        ];
198        let effective = caps::read(None, CapSet::Effective).map_err(|e| {
199            NucleusError::CapabilityError(format!("Failed to read effective caps: {}", e))
200        })?;
201        let mut leaked = Vec::new();
202        for cap in &ns_caps {
203            if effective.contains(cap) {
204                leaked.push(format!("{:?}", cap));
205            }
206        }
207        if !leaked.is_empty() {
208            let msg = format!(
209                "SEC-CLONE3: namespace-creating capabilities still present after drop: [{}]. \
210                 clone3 syscall is allowed without argument filtering — these caps \
211                 must be absent to prevent namespace escape.",
212                leaked.join(", ")
213            );
214            if production {
215                return Err(NucleusError::CapabilityError(msg));
216            }
217            tracing::warn!("{}", msg);
218        }
219        Ok(())
220    }
221}
222
223impl Default for CapabilityManager {
224    fn default() -> Self {
225        Self::new()
226    }
227}
228
229fn to_caps_hash_set(caps_list: &[Capability]) -> CapsHashSet {
230    caps_list.iter().copied().collect()
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    #[test]
238    fn test_capability_manager_initial_state() {
239        let mgr = CapabilityManager::new();
240        assert!(!mgr.is_dropped());
241    }
242
243    #[test]
244    fn test_drop_idempotent() {
245        let mut mgr = CapabilityManager::new();
246        // First drop may fail in unprivileged test environments (M4 verification).
247        // That's expected — the important thing is idempotency of the dropped flag.
248        match mgr.drop_all() {
249            Ok(()) => {
250                assert!(mgr.is_dropped());
251                // Second drop should also succeed (idempotent)
252                let result = mgr.drop_all();
253                assert!(result.is_ok());
254                assert!(mgr.is_dropped());
255            }
256            Err(_) => {
257                // In unprivileged tests, bounding set verification may fail.
258                // This is expected and not a test failure.
259            }
260        }
261    }
262}