diff --git a/crates/stackable-operator/CHANGELOG.md b/crates/stackable-operator/CHANGELOG.md index ee60b2114..2bb2dd2bb 100644 --- a/crates/stackable-operator/CHANGELOG.md +++ b/crates/stackable-operator/CHANGELOG.md @@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Changed + +- `SecurityContextBuilder::with_stackable_defaults` sets `allowPrivilegeEscalation: false` and + `capabilities.drop: [ALL]`, and `PodSecurityContextBuilder::with_stackable_defaults` additionally + sets `seccompProfile.type: RuntimeDefault` ([#1292]). + Together these satisfy the `restricted` Pod Security Standard, which plain Kubernetes enforces + only if the namespace opts in and which OpenShift's SCCs inject during admission either way, so + the Pods OpenShift ends up running are unchanged. + And because OpenShift has already applied these for years we're pretty sure that this change is safe for us. + Operators already call `PodSecurityContextBuilder::with_stackable_defaults`, so the + `seccompProfile` default reaches their Pods with the dependency bump alone, nothing else to do. + `allowPrivilegeEscalation` and `capabilities` have no Pod-level equivalent, so a Pod is only + covered once every one of its containers is built with `SecurityContextBuilder`. + +[#1292]: https://github.com/stackabletech/operator-rs/pull/1292 + ## [0.119.0] - 2026-09-23 ### Removed diff --git a/crates/stackable-operator/src/builder/pod/security.rs b/crates/stackable-operator/src/builder/pod/security.rs index bff89f332..846297f8e 100644 --- a/crates/stackable-operator/src/builder/pod/security.rs +++ b/crates/stackable-operator/src/builder/pod/security.rs @@ -14,15 +14,30 @@ pub struct SecurityContextBuilder { impl SecurityContextBuilder { /// Construct a new [`SecurityContextBuilder`] that is pre-filled with Stackable's defaults. /// - /// We currently don't have any defaults we set. + /// The defaults are: /// - /// We intentionally don't set `runAsNonRoot`, as we set that in + /// * `allowPrivilegeEscalation: false` + /// * `capabilities.drop: [ALL]` + /// + /// Neither field exists on [`PodSecurityContext`], + /// so both have to be set on every container to take effect. + /// + /// We intentionally don't set `runAsNonRoot` or `seccompProfile`, as we set those in /// [`PodSecurityContextBuilder::with_stackable_defaults`] already and don't want to confuse - /// users by setting it on the Pod and container. + /// users by setting them on the Pod and container. pub fn with_stackable_defaults() -> Self { - Self { + let mut builder = Self { security_context: SecurityContext::default(), - } + }; + + builder + .allow_privilege_escalation(false) + .capabilities(Capabilities { + drop: Some(vec!["ALL".to_owned()]), + ..Capabilities::default() + }); + + builder } pub fn allow_privilege_escalation(&mut self, value: bool) -> &mut Self { @@ -175,6 +190,10 @@ impl PodSecurityContextBuilder { /// Currently the defaults are: /// /// * `runAsNonRoot: true` + /// * `seccompProfile.type: RuntimeDefault` + /// + /// `seccompProfile` is set here rather than per container so that it also covers containers + /// built elsewhere. pub fn with_stackable_defaults() -> Self { // We are using the builder functions to ensure that builder functions exist to override these settings. let mut builder = Self { @@ -184,6 +203,10 @@ impl PodSecurityContextBuilder { // Reason: Running as root is bad builder.run_as_non_root(true); + // Reason: The runtime's default profile blocks dangerous syscalls without breaking + // ordinary applications. + builder.seccomp_profile_type("RuntimeDefault"); + builder } @@ -435,6 +458,44 @@ mod tests { run_as_non_root: Some(true), run_as_user: Some(1001), run_as_group: Some(1001), + capabilities: Some(Capabilities { + drop: Some(vec!["ALL".to_owned()]), + ..Default::default() + }), + ..Default::default() + } + ); + } + + #[test] + fn security_context_builder_defaults() { + let context = SecurityContextBuilder::with_stackable_defaults().build(); + + assert_eq!( + context, + SecurityContext { + allow_privilege_escalation: Some(false), + capabilities: Some(Capabilities { + drop: Some(vec!["ALL".to_owned()]), + ..Default::default() + }), + ..Default::default() + } + ); + } + + #[test] + fn pod_security_context_builder_defaults() { + let context = PodSecurityContextBuilder::with_stackable_defaults().build(); + + assert_eq!( + context, + PodSecurityContext { + run_as_non_root: Some(true), + seccomp_profile: Some(SeccompProfile { + type_: "RuntimeDefault".to_owned(), + ..Default::default() + }), ..Default::default() } );