From 43dcdc19a16552c0ab527b21154b2dd3ad238f4d Mon Sep 17 00:00:00 2001 From: Alex Caston Date: Tue, 28 Apr 2026 17:17:00 +0200 Subject: [PATCH] Kubernetes: Support allowPrivilegeEscalation and capabilities backend_options (#6307) --- .../11-backends/20-kubernetes.md | 20 ++++++ .../backend/kubernetes/backend_options.go | 22 +++--- pipeline/backend/kubernetes/pod.go | 56 ++++++++++----- pipeline/backend/kubernetes/pod_test.go | 71 +++++++++++++++++-- 4 files changed, 138 insertions(+), 31 deletions(-) diff --git a/docs/docs/30-administration/10-configuration/11-backends/20-kubernetes.md b/docs/docs/30-administration/10-configuration/11-backends/20-kubernetes.md index 7de7054dab..a6f43a683a 100644 --- a/docs/docs/30-administration/10-configuration/11-backends/20-kubernetes.md +++ b/docs/docs/30-administration/10-configuration/11-backends/20-kubernetes.md @@ -359,6 +359,26 @@ backend_options: The feature requires Kubernetes v1.30 or above. ::: +You can set `allowPrivilegeEscalation` to `false` to prevent a container from gaining more privileges than its parent process. + +```yaml +backend_options: + kubernetes: + securityContext: + allowPrivilegeEscalation: false +``` + +You can also drop [Linux capabilities](https://man7.org/linux/man-pages/man7/capabilities.7.html) from a container. Adding capabilities is not allowed. + +```yaml +backend_options: + kubernetes: + securityContext: + capabilities: + drop: + - ALL +``` + ### Annotations and labels You can specify arbitrary [annotations](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/) and [labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) to be set on the Pod definition for a given workflow step using the following configuration: diff --git a/pipeline/backend/kubernetes/backend_options.go b/pipeline/backend/kubernetes/backend_options.go index 75559082b3..5f37f2690a 100644 --- a/pipeline/backend/kubernetes/backend_options.go +++ b/pipeline/backend/kubernetes/backend_options.go @@ -66,14 +66,16 @@ const ( ) type SecurityContext struct { - Privileged *bool `mapstructure:"privileged"` - RunAsNonRoot *bool `mapstructure:"runAsNonRoot"` - RunAsUser *int64 `mapstructure:"runAsUser"` - RunAsGroup *int64 `mapstructure:"runAsGroup"` - FSGroup *int64 `mapstructure:"fsGroup"` - FsGroupChangePolicy *kube_core_v1.PodFSGroupChangePolicy `mapstructure:"fsGroupChangePolicy"` - SeccompProfile *SecProfile `mapstructure:"seccompProfile"` - ApparmorProfile *SecProfile `mapstructure:"apparmorProfile"` + Privileged *bool `mapstructure:"privileged"` + RunAsNonRoot *bool `mapstructure:"runAsNonRoot"` + RunAsUser *int64 `mapstructure:"runAsUser"` + RunAsGroup *int64 `mapstructure:"runAsGroup"` + FSGroup *int64 `mapstructure:"fsGroup"` + FsGroupChangePolicy *kube_core_v1.PodFSGroupChangePolicy `mapstructure:"fsGroupChangePolicy"` + SeccompProfile *SecProfile `mapstructure:"seccompProfile"` + ApparmorProfile *SecProfile `mapstructure:"apparmorProfile"` + AllowPrivilegeEscalation *bool `mapstructure:"allowPrivilegeEscalation"` + Capabilities *Capabilities `mapstructure:"capabilities"` } type SecProfile struct { @@ -83,6 +85,10 @@ type SecProfile struct { type SecProfileType string +type Capabilities struct { + Drop []string `mapstructure:"drop"` +} + // SecretRef defines Kubernetes secret reference. type SecretRef struct { Name string `mapstructure:"name"` diff --git a/pipeline/backend/kubernetes/pod.go b/pipeline/backend/kubernetes/pod.go index f59c99e30e..88960a41e3 100644 --- a/pipeline/backend/kubernetes/pod.go +++ b/pipeline/backend/kubernetes/pod.go @@ -590,33 +590,57 @@ func apparmorProfile(scp *SecProfile) *kube_core_v1.AppArmorProfile { return apparmorProfile } -func containerSecurityContext(sc *SecurityContext, stepPrivileged bool) *kube_core_v1.SecurityContext { - if !stepPrivileged { +func containerCapabilities(capabilities *Capabilities) *kube_core_v1.Capabilities { + if capabilities == nil || len(capabilities.Drop) == 0 { return nil } - //nolint:staticcheck - privileged := false + drop := make([]kube_core_v1.Capability, len(capabilities.Drop)) - // if security context privileged is set explicitly - if sc != nil && sc.Privileged != nil && *sc.Privileged { - privileged = true + for i, c := range capabilities.Drop { + drop[i] = kube_core_v1.Capability(c) } - // if security context privileged is not set explicitly, but step is privileged - if (sc == nil || sc.Privileged == nil) && stepPrivileged { - privileged = true + return &kube_core_v1.Capabilities{ + Drop: drop, + } +} + +func containerSecurityContext(sc *SecurityContext, stepPrivileged bool) *kube_core_v1.SecurityContext { + var ( + privileged *bool + allowPrivilegeEscalation *bool + capabilities *kube_core_v1.Capabilities + ) + + // A container may only run privileged when the step itself is privileged. + // If the step is privileged, the container is privileged by default unless + // explicitly disabled via securityContext.privileged=false. + if stepPrivileged && (sc == nil || sc.Privileged == nil || *sc.Privileged) { + privileged = newBool(true) } - if privileged { - securityContext := &kube_core_v1.SecurityContext{ - Privileged: newBool(true), + if sc != nil { + // allowPrivilegeEscalation can only be set to false. + if sc.AllowPrivilegeEscalation != nil && !*sc.AllowPrivilegeEscalation { + allowPrivilegeEscalation = sc.AllowPrivilegeEscalation } - log.Trace().Msgf("container security context that will be used: %v", securityContext) - return securityContext + + capabilities = containerCapabilities(sc.Capabilities) } - return nil + if privileged == nil && capabilities == nil && allowPrivilegeEscalation == nil { + return nil + } + + securityContext := &kube_core_v1.SecurityContext{ + Privileged: privileged, + AllowPrivilegeEscalation: allowPrivilegeEscalation, + Capabilities: capabilities, + } + + log.Trace().Msgf("container security context that will be used: %v", securityContext) + return securityContext } func mapToEnvVars(m map[string]string) []kube_core_v1.EnvVar { diff --git a/pipeline/backend/kubernetes/pod_test.go b/pipeline/backend/kubernetes/pod_test.go index da492f26c7..7a4938e815 100644 --- a/pipeline/backend/kubernetes/pod_test.go +++ b/pipeline/backend/kubernetes/pod_test.go @@ -301,7 +301,11 @@ func TestFullPod(t *testing.T) { ], "imagePullPolicy": "Always", "securityContext": { - "privileged": true + "privileged": true, + "allowPrivilegeEscalation": false, + "capabilities": { + "drop": ["ALL"] + } } } ], @@ -379,12 +383,16 @@ func TestFullPod(t *testing.T) { } fsGroupChangePolicy := kube_core_v1.PodFSGroupChangePolicy("OnRootMismatch") secCtx := SecurityContext{ - Privileged: newBool(true), - RunAsNonRoot: newBool(true), - RunAsUser: newInt64(101), - RunAsGroup: newInt64(101), - FSGroup: newInt64(101), - FsGroupChangePolicy: &fsGroupChangePolicy, + Privileged: newBool(true), + RunAsNonRoot: newBool(true), + RunAsUser: newInt64(101), + RunAsGroup: newInt64(101), + FSGroup: newInt64(101), + FsGroupChangePolicy: &fsGroupChangePolicy, + AllowPrivilegeEscalation: newBool(false), + Capabilities: &Capabilities{ + Drop: []string{"ALL"}, + }, SeccompProfile: &SecProfile{ Type: "Localhost", LocalhostProfile: "profiles/audit.json", @@ -527,6 +535,55 @@ func TestPodPrivilege(t *testing.T) { pod, err = createTestPod(false, true, secCtx) assert.NoError(t, err) assert.True(t, *pod.Spec.SecurityContext.RunAsNonRoot) + + // non-privileged step with allowPrivilegeEscalation=false: applied + secCtx = SecurityContext{ + AllowPrivilegeEscalation: newBool(false), + } + pod, err = createTestPod(false, false, secCtx) + assert.NoError(t, err) + assert.NotNil(t, pod.Spec.Containers[0].SecurityContext) + assert.False(t, *pod.Spec.Containers[0].SecurityContext.AllowPrivilegeEscalation) + assert.Nil(t, pod.Spec.Containers[0].SecurityContext.Privileged) + + // non-privileged step with allowPrivilegeEscalation=true: ignored + secCtx = SecurityContext{ + AllowPrivilegeEscalation: newBool(true), + } + pod, err = createTestPod(false, false, secCtx) + assert.NoError(t, err) + assert.Nil(t, pod.Spec.Containers[0].SecurityContext) + + // privileged step with allowPrivilegeEscalation=true: ignored + secCtx = SecurityContext{ + AllowPrivilegeEscalation: newBool(true), + } + pod, err = createTestPod(true, false, secCtx) + assert.NoError(t, err) + assert.Nil(t, pod.Spec.Containers[0].SecurityContext.AllowPrivilegeEscalation) + + // non-privileged step with capabilities drop: applied + secCtx = SecurityContext{ + Capabilities: &Capabilities{Drop: []string{"ALL"}}, + } + pod, err = createTestPod(false, false, secCtx) + assert.NoError(t, err) + assert.NotNil(t, pod.Spec.Containers[0].SecurityContext) + assert.Equal(t, []kube_core_v1.Capability{"ALL"}, pod.Spec.Containers[0].SecurityContext.Capabilities.Drop) + assert.Nil(t, pod.Spec.Containers[0].SecurityContext.Capabilities.Add) + assert.Nil(t, pod.Spec.Containers[0].SecurityContext.Privileged) + + // non-privileged step with drop capabilities and allowPrivilegeEscalation=false: both applied + secCtx = SecurityContext{ + AllowPrivilegeEscalation: newBool(false), + Capabilities: &Capabilities{Drop: []string{"ALL"}}, + } + pod, err = createTestPod(false, false, secCtx) + assert.NoError(t, err) + assert.NotNil(t, pod.Spec.Containers[0].SecurityContext) + assert.Nil(t, pod.Spec.Containers[0].SecurityContext.Privileged) + assert.False(t, *pod.Spec.Containers[0].SecurityContext.AllowPrivilegeEscalation) + assert.Equal(t, []kube_core_v1.Capability{"ALL"}, pod.Spec.Containers[0].SecurityContext.Capabilities.Drop) } func TestScratchPod(t *testing.T) {