From 338e3610502cdd39912ea6c3e570d9d2a4942abe Mon Sep 17 00:00:00 2001 From: Lars Francke Date: Thu, 17 Sep 2026 12:51:50 +0200 Subject: [PATCH] feat(operator): Add security context defaults This makes our containers compliant with the "restricted" Pod Security Standard and any admission policies that require it. OpenShift already injects these today (and has done so in the past) so that's why I'm pretty certain our operators & products can deal with these. --- crates/stackable-operator/CHANGELOG.md | 16 +++++ .../src/builder/pod/security.rs | 71 +++++++++++++++++-- 2 files changed, 82 insertions(+), 5 deletions(-) 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() } );