1use k8s_openapi::api::core::v1::{Capabilities, PodSecurityContext, SecurityContext};
44
45use super::effective_run_as_user;
46
47pub const CAP_DAC_OVERRIDE: &str = "DAC_OVERRIDE";
49pub const CAP_DAC_READ_SEARCH: &str = "DAC_READ_SEARCH";
52pub const CAP_CHOWN: &str = "CHOWN";
54pub const CAP_FOWNER: &str = "FOWNER";
56pub const CAP_FSETID: &str = "FSETID";
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum MoverOperation {
64 Backup,
66 Restore,
68 Maintenance,
70 Verify,
72 Bootstrap,
74 RepositoryReplicate,
76 SnapshotReplicate,
78 SnapshotDelete,
80 SnapshotPin,
82 Browse,
84}
85
86impl MoverOperation {
87 pub fn privileged_capabilities(self) -> &'static [&'static str] {
90 match self {
91 MoverOperation::Backup => &[CAP_DAC_OVERRIDE],
93 MoverOperation::Restore => &[CAP_CHOWN, CAP_DAC_OVERRIDE, CAP_FOWNER, CAP_FSETID],
97 MoverOperation::Maintenance
102 | MoverOperation::Verify
103 | MoverOperation::Bootstrap
104 | MoverOperation::RepositoryReplicate
105 | MoverOperation::SnapshotReplicate
106 | MoverOperation::SnapshotDelete
107 | MoverOperation::SnapshotPin
108 | MoverOperation::Browse => &[],
109 }
110 }
111}
112
113#[derive(Debug, Clone, PartialEq, Eq)]
115pub enum PrivilegedModeEffect {
116 NotRequested,
118 NothingToGrant,
120 Granted,
122 InertNonRootUid {
124 uid: i64,
126 },
127 InertUnsetUid,
130}
131
132impl PrivilegedModeEffect {
133 pub fn warning(&self) -> Option<String> {
135 match self {
136 PrivilegedModeEffect::InertNonRootUid { uid } => Some(format!(
137 "privilegedMode grants capabilities only to a root mover; this one runs as \
138 UID {uid}. Fix: set securityContext.runAsUser: 0."
139 )),
140 PrivilegedModeEffect::InertUnsetUid => Some(
141 "privilegedMode grants capabilities only to a root mover. Fix: set \
142 securityContext.runAsUser: 0."
143 .to_string(),
144 ),
145 PrivilegedModeEffect::NotRequested
146 | PrivilegedModeEffect::NothingToGrant
147 | PrivilegedModeEffect::Granted => None,
148 }
149 }
150}
151
152pub fn privileged_mode_effect(
155 sc: &SecurityContext,
156 psc: Option<&PodSecurityContext>,
157 privileged_mode: Option<bool>,
158 op: MoverOperation,
159) -> PrivilegedModeEffect {
160 if privileged_mode != Some(true) {
161 return PrivilegedModeEffect::NotRequested;
162 }
163 if op.privileged_capabilities().is_empty() {
164 return PrivilegedModeEffect::NothingToGrant;
165 }
166 match effective_run_as_user(Some(sc), psc) {
167 None => PrivilegedModeEffect::InertUnsetUid,
168 Some(0) => PrivilegedModeEffect::Granted,
169 Some(uid) => PrivilegedModeEffect::InertNonRootUid { uid },
170 }
171}
172
173pub fn apply_privileged_mode(
176 mut sc: SecurityContext,
177 psc: Option<PodSecurityContext>,
178 privileged_mode: Option<bool>,
179 op: MoverOperation,
180) -> (
181 SecurityContext,
182 Option<PodSecurityContext>,
183 PrivilegedModeEffect,
184) {
185 let effect = privileged_mode_effect(&sc, psc.as_ref(), privileged_mode, op);
186 match &effect {
187 PrivilegedModeEffect::Granted => {
188 sc.capabilities = Some(add_capabilities(
189 sc.capabilities.take(),
190 op.privileged_capabilities(),
191 ));
192 }
193 PrivilegedModeEffect::NotRequested
194 | PrivilegedModeEffect::NothingToGrant
195 | PrivilegedModeEffect::InertNonRootUid { .. }
196 | PrivilegedModeEffect::InertUnsetUid => {}
197 }
198 (sc, psc, effect)
199}
200
201fn add_capabilities(caps: Option<Capabilities>, grant: &[&str]) -> Capabilities {
205 let mut caps = caps.unwrap_or_default();
206 let mut add = caps.add.take().unwrap_or_default();
207 for cap in grant {
208 if !add.iter().any(|have| same_capability(have, cap)) {
209 add.push((*cap).to_string());
210 }
211 }
212 caps.add = Some(add);
213 caps
214}
215
216pub fn same_capability(a: &str, b: &str) -> bool {
219 fn norm(s: &str) -> String {
220 let up = s.trim().to_ascii_uppercase();
221 up.strip_prefix("CAP_").map(str::to_string).unwrap_or(up)
222 }
223 norm(a) == norm(b)
224}
225
226pub fn bypasses_read_permission_checks(sc: &SecurityContext) -> bool {
230 sc.privileged == Some(true)
231 || sc
232 .capabilities
233 .as_ref()
234 .and_then(|c| c.add.as_ref())
235 .is_some_and(|add| {
236 add.iter().any(|c| {
237 same_capability(c, CAP_DAC_OVERRIDE) || same_capability(c, CAP_DAC_READ_SEARCH)
238 })
239 })
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245 use crate::common::{ResolvedMover, resolve_mover};
246
247 fn root_sc() -> SecurityContext {
248 SecurityContext {
249 run_as_user: Some(0),
250 run_as_non_root: Some(false),
251 ..Default::default()
252 }
253 }
254
255 fn resolved(
256 sc: Option<&SecurityContext>,
257 privileged: Option<bool>,
258 op: MoverOperation,
259 ) -> ResolvedMover {
260 resolve_mover(None, sc, None, None, None, None).with_privileged_mode(privileged, op)
261 }
262
263 fn adds(m: &ResolvedMover) -> Vec<String> {
264 m.security_context
265 .capabilities
266 .as_ref()
267 .and_then(|c| c.add.clone())
268 .unwrap_or_default()
269 }
270
271 fn drops(m: &ResolvedMover) -> Vec<String> {
272 m.security_context
273 .capabilities
274 .as_ref()
275 .and_then(|c| c.drop.clone())
276 .unwrap_or_default()
277 }
278
279 #[test]
283 fn privileged_root_backup_gets_dac_override_and_still_drops_all() {
284 let m = resolved(Some(&root_sc()), Some(true), MoverOperation::Backup);
285 assert_eq!(adds(&m), vec!["DAC_OVERRIDE"]);
286 assert_eq!(drops(&m), vec!["ALL"]);
287 assert_eq!(m.security_context.run_as_user, Some(0));
288 assert_eq!(m.security_context.run_as_non_root, Some(false));
289 assert_eq!(m.security_context.allow_privilege_escalation, Some(false));
290 }
291
292 #[test]
293 fn privileged_root_restore_gets_the_four_ownership_capabilities() {
294 let m = resolved(Some(&root_sc()), Some(true), MoverOperation::Restore);
295 assert_eq!(adds(&m), vec!["CHOWN", "DAC_OVERRIDE", "FOWNER", "FSETID"]);
296 assert_eq!(drops(&m), vec!["ALL"]);
297 }
298
299 #[test]
301 fn non_privileged_root_gets_no_capabilities() {
302 for op in [MoverOperation::Backup, MoverOperation::Restore] {
303 for pm in [None, Some(false)] {
304 let m = resolved(Some(&root_sc()), pm, op);
305 assert!(adds(&m).is_empty(), "{op:?} {pm:?}");
306 assert_eq!(drops(&m), vec!["ALL"]);
307 }
308 }
309 }
310
311 #[test]
312 fn user_added_capabilities_are_kept_and_unioned() {
313 let mut sc = root_sc();
314 sc.capabilities = Some(Capabilities {
315 add: Some(vec!["SETUID".into(), "cap_dac_override".into()]),
316 drop: None,
317 });
318 let m = resolved(Some(&sc), Some(true), MoverOperation::Restore);
319 assert_eq!(
320 adds(&m),
321 vec!["SETUID", "cap_dac_override", "CHOWN", "FOWNER", "FSETID"],
322 "nothing the user asked for is removed and DAC_OVERRIDE is not duplicated"
323 );
324 assert_eq!(
325 drops(&m),
326 vec!["ALL"],
327 "the hardened drop:[ALL] survives an add-only override"
328 );
329 }
330
331 #[test]
335 fn privileged_with_no_pinned_uid_keeps_the_uid_grants_nothing_and_warns() {
336 for op in [MoverOperation::Backup, MoverOperation::Restore] {
337 let base = resolve_mover(None, None, None, None, None, None);
338 let (m, effect) = base.clone().with_privileged_mode_effect(Some(true), op);
339 assert_eq!(m, base, "{op:?}: uid/runAsNonRoot/caps unchanged");
340 assert_eq!(m.security_context.run_as_user, None);
341 assert_eq!(m.security_context.run_as_non_root, Some(true));
342 assert!(adds(&m).is_empty());
343 assert_eq!(effect, PrivilegedModeEffect::InertUnsetUid);
344 let msg = effect.warning().expect("inert privilegedMode warns");
345 assert!(
346 msg.contains("root mover") && msg.contains("Fix: set securityContext.runAsUser: 0"),
347 "{msg}"
348 );
349 }
350 }
351
352 #[test]
354 fn privileged_with_a_pinned_non_root_uid_grants_nothing_and_warns() {
355 let sc = SecurityContext {
356 run_as_user: Some(1000),
357 ..Default::default()
358 };
359 let base = resolve_mover(None, Some(&sc), None, None, None, None);
360 let (out, _, effect) = apply_privileged_mode(
361 base.security_context.clone(),
362 base.pod_security_context.clone(),
363 Some(true),
364 MoverOperation::Backup,
365 );
366 assert_eq!(out, base.security_context);
367 assert_eq!(effect, PrivilegedModeEffect::InertNonRootUid { uid: 1000 });
368 let msg = effect.warning().expect("inert privilegedMode warns");
369 assert!(msg.contains("UID 1000") && msg.contains("Fix:"), "{msg}");
370 }
371
372 #[test]
374 fn pod_level_root_is_granted() {
375 let psc = PodSecurityContext {
376 run_as_user: Some(0),
377 ..Default::default()
378 };
379 let m = resolve_mover(None, None, Some(&psc), None, None, None)
380 .with_privileged_mode(Some(true), MoverOperation::Backup);
381 assert_eq!(adds(&m), vec!["DAC_OVERRIDE"]);
382 assert_eq!(m.security_context.run_as_user, Some(0));
383 }
384
385 #[test]
386 fn repository_only_operations_grant_nothing() {
387 for op in [
388 MoverOperation::Maintenance,
389 MoverOperation::Verify,
390 MoverOperation::Bootstrap,
391 MoverOperation::RepositoryReplicate,
392 MoverOperation::SnapshotReplicate,
393 MoverOperation::SnapshotDelete,
394 MoverOperation::SnapshotPin,
395 MoverOperation::Browse,
396 ] {
397 let base = resolve_mover(None, Some(&root_sc()), None, None, None, None);
398 let m = base.clone().with_privileged_mode(Some(true), op);
399 assert_eq!(m, base, "{op:?}");
400 }
401 }
402
403 #[test]
404 fn admission_warning_unless_the_spec_pins_root() {
405 use crate::common::MoverSpec;
406 let non_root = MoverSpec {
407 security_context: Some(SecurityContext {
408 run_as_user: Some(1000),
409 ..Default::default()
410 }),
411 privileged_mode: Some(true),
412 ..Default::default()
413 };
414 assert!(
415 non_root
416 .privileged_mode_warning(MoverOperation::Backup)
417 .is_some()
418 );
419 assert!(
421 non_root
422 .privileged_mode_warning(MoverOperation::Maintenance)
423 .is_none()
424 );
425 let root = MoverSpec {
426 security_context: Some(root_sc()),
427 privileged_mode: Some(true),
428 ..Default::default()
429 };
430 assert!(
431 root.privileged_mode_warning(MoverOperation::Restore)
432 .is_none()
433 );
434 let unset = MoverSpec {
435 privileged_mode: Some(true),
436 ..Default::default()
437 };
438 assert!(
443 unset
444 .privileged_mode_warning(MoverOperation::Backup)
445 .is_none(),
446 "admission must not guess about a uid moverDefaults may pin"
447 );
448 let off = MoverSpec {
449 privileged_mode: Some(false),
450 ..non_root.clone()
451 };
452 assert!(
453 off.privileged_mode_warning(MoverOperation::Backup)
454 .is_none()
455 );
456 }
457
458 #[test]
459 fn apply_is_idempotent() {
460 for op in [MoverOperation::Backup, MoverOperation::Restore] {
461 let once = resolved(Some(&root_sc()), Some(true), op);
462 let twice = once.clone().with_privileged_mode(Some(true), op);
463 assert_eq!(once, twice, "{op:?}");
464 }
465 }
466
467 #[test]
469 fn every_grant_is_pss_baseline() {
470 const BASELINE: &[&str] = &[
471 "AUDIT_WRITE",
472 "CHOWN",
473 "DAC_OVERRIDE",
474 "FOWNER",
475 "FSETID",
476 "KILL",
477 "MKNOD",
478 "NET_BIND_SERVICE",
479 "SETFCAP",
480 "SETGID",
481 "SETPCAP",
482 "SETUID",
483 "SYS_CHROOT",
484 ];
485 for op in [MoverOperation::Backup, MoverOperation::Restore] {
486 for cap in op.privileged_capabilities() {
487 assert!(BASELINE.contains(cap), "{cap} is not PSS baseline");
488 }
489 }
490 }
491
492 #[test]
493 fn read_bypass_detection() {
494 let mut sc = root_sc();
495 assert!(!bypasses_read_permission_checks(&sc));
496 for cap in ["DAC_OVERRIDE", "CAP_DAC_READ_SEARCH", "dac_override"] {
497 sc.capabilities = Some(Capabilities {
498 add: Some(vec![cap.into()]),
499 drop: Some(vec!["ALL".into()]),
500 });
501 assert!(bypasses_read_permission_checks(&sc), "{cap}");
502 }
503 sc.capabilities = Some(Capabilities {
504 add: Some(vec!["CHOWN".into()]),
505 drop: None,
506 });
507 assert!(!bypasses_read_permission_checks(&sc));
508 let privileged = SecurityContext {
509 privileged: Some(true),
510 ..Default::default()
511 };
512 assert!(bypasses_read_permission_checks(&privileged));
513 }
514}