Skip to content

Commit 75ac6cf

Browse files
committed
feat(security): enforce pod and container security contexts
Apply default pod/container security contexts across KFP v2 compiler templates (driver, executor, importer, DAG driver) and workflow spec. Harden control-plane manifests/charts with runAsNonRoot and RuntimeDefault seccomp profiles, and update test workflows/goldens.
1 parent dace02d commit 75ac6cf

33 files changed

+409
-10
lines changed

backend/src/v2/compiler/argocompiler/argo.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,10 @@ func Compile(jobArg *pipelinespec.PipelineJob, kubernetesSpecArg *pipelinespec.S
150150
},
151151
}
152152

153+
wf.Spec.SecurityContext = defaultPodSecurityContext()
153154
runAsUser := GetPipelineRunAsUser()
154155
if runAsUser != nil {
155-
wf.Spec.SecurityContext = &k8score.PodSecurityContext{RunAsUser: runAsUser}
156+
wf.Spec.SecurityContext.RunAsUser = runAsUser
156157
}
157158

158159
c := &workflowCompiler{

backend/src/v2/compiler/argocompiler/container.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ func (c *workflowCompiler) addContainerDriverTemplate() string {
265265
Env: append(proxy.GetConfig().GetEnvVars(), commonEnvs...),
266266
},
267267
}
268+
applySecurityContextToTemplate(t)
268269
// If TLS is enabled (apiserver or metadata), add the custom CA bundle to the container driver template.
269270
if setCABundle {
270271
ConfigureCustomCABundle(t)
@@ -555,6 +556,7 @@ func (c *workflowCompiler) addContainerExecutorTemplate(task *pipelinespec.Pipel
555556
if common.GetCaBundleSecretName() != "" || common.GetCaBundleConfigMapName() != "" {
556557
ConfigureCustomCABundle(executor)
557558
}
559+
applySecurityContextToTemplate(executor)
558560

559561
// If retry policy is set, add retryStrategy to executor
560562
if taskRetrySpec != nil {

backend/src/v2/compiler/argocompiler/dag.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,7 @@ func (c *workflowCompiler) addDAGDriverTemplate() string {
630630
Env: proxy.GetConfig().GetEnvVars(),
631631
},
632632
}
633+
applySecurityContextToTemplate(t)
633634
// If TLS is enabled (apiserver or metadata), add the custom CA bundle to the DAG driver template.
634635
if setCABundle {
635636
ConfigureCustomCABundle(t)

backend/src/v2/compiler/argocompiler/importer.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ func (c *workflowCompiler) addImporterTemplate(downloadToWorkspace bool) string
152152
if setCABundle {
153153
ConfigureCustomCABundle(importerTemplate)
154154
}
155+
applySecurityContextToTemplate(importerTemplate)
155156
c.templates[name] = importerTemplate
156157
c.wf.Spec.Templates = append(c.wf.Spec.Templates, *importerTemplate)
157158
return name
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright 2026 The Kubeflow Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package argocompiler
16+
17+
import (
18+
wfapi "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1"
19+
k8score "k8s.io/api/core/v1"
20+
)
21+
22+
func defaultPodSecurityContext() *k8score.PodSecurityContext {
23+
runAsNonRoot := true
24+
return &k8score.PodSecurityContext{
25+
RunAsNonRoot: &runAsNonRoot,
26+
SeccompProfile: &k8score.SeccompProfile{
27+
Type: k8score.SeccompProfileTypeRuntimeDefault,
28+
},
29+
}
30+
}
31+
32+
func defaultContainerSecurityContext() *k8score.SecurityContext {
33+
allowPrivilegeEscalation := false
34+
runAsNonRoot := true
35+
return &k8score.SecurityContext{
36+
AllowPrivilegeEscalation: &allowPrivilegeEscalation,
37+
RunAsNonRoot: &runAsNonRoot,
38+
Capabilities: &k8score.Capabilities{
39+
Drop: []k8score.Capability{"ALL"},
40+
},
41+
SeccompProfile: &k8score.SeccompProfile{
42+
Type: k8score.SeccompProfileTypeRuntimeDefault,
43+
},
44+
}
45+
}
46+
47+
func applySecurityContextToTemplate(t *wfapi.Template) {
48+
if t == nil {
49+
return
50+
}
51+
applyPodSecurityContext(&t.SecurityContext)
52+
applySecurityContextToContainer(t.Container)
53+
applySecurityContextToUserContainers(t.InitContainers)
54+
applySecurityContextToUserContainers(t.Sidecars)
55+
}
56+
57+
func applyPodSecurityContext(sc **k8score.PodSecurityContext) {
58+
if *sc == nil {
59+
*sc = defaultPodSecurityContext()
60+
return
61+
}
62+
runAsNonRoot := true
63+
(*sc).RunAsNonRoot = &runAsNonRoot
64+
if (*sc).SeccompProfile == nil {
65+
(*sc).SeccompProfile = &k8score.SeccompProfile{
66+
Type: k8score.SeccompProfileTypeRuntimeDefault,
67+
}
68+
}
69+
}
70+
71+
func applySecurityContextToContainer(c *k8score.Container) {
72+
if c == nil {
73+
return
74+
}
75+
if c.SecurityContext == nil {
76+
c.SecurityContext = defaultContainerSecurityContext()
77+
return
78+
}
79+
allowPrivilegeEscalation := false
80+
runAsNonRoot := true
81+
c.SecurityContext.AllowPrivilegeEscalation = &allowPrivilegeEscalation
82+
c.SecurityContext.RunAsNonRoot = &runAsNonRoot
83+
c.SecurityContext.Capabilities = &k8score.Capabilities{
84+
Drop: []k8score.Capability{"ALL"},
85+
}
86+
if c.SecurityContext.SeccompProfile == nil {
87+
c.SecurityContext.SeccompProfile = &k8score.SeccompProfile{
88+
Type: k8score.SeccompProfileTypeRuntimeDefault,
89+
}
90+
}
91+
}
92+
93+
func applySecurityContextToUserContainers(containers []wfapi.UserContainer) {
94+
for i := range containers {
95+
applySecurityContextToContainer(&containers[i].Container)
96+
}
97+
}

backend/src/v2/compiler/argocompiler/spec_patch_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ func TestApplyWorkflowSpecPatch_ComplexPatch(t *testing.T) {
199199
compiler := &workflowCompiler{
200200
wf: wf,
201201
}
202+
wf.Spec.SecurityContext = defaultPodSecurityContext()
202203

203204
complexPatch := `{
204205
"serviceAccountName": "complex-sa",
@@ -243,6 +244,10 @@ func TestApplyWorkflowSpecPatch_ComplexPatch(t *testing.T) {
243244
assert.Equal(t, int64(1000), *wf.Spec.SecurityContext.RunAsUser)
244245
assert.Equal(t, int64(1000), *wf.Spec.SecurityContext.RunAsGroup)
245246
assert.Equal(t, int64(1000), *wf.Spec.SecurityContext.FSGroup)
247+
assert.NotNil(t, wf.Spec.SecurityContext.RunAsNonRoot)
248+
assert.Equal(t, true, *wf.Spec.SecurityContext.RunAsNonRoot)
249+
assert.NotNil(t, wf.Spec.SecurityContext.SeccompProfile)
250+
assert.Equal(t, corev1.SeccompProfileTypeRuntimeDefault, wf.Spec.SecurityContext.SeccompProfile.Type)
246251
assert.Equal(t, false, *wf.Spec.HostNetwork)
247252
assert.Equal(t, corev1.DNSClusterFirst, *wf.Spec.DNSPolicy)
248253
}

backend/test/compiler/matchers/workflow_matcher.go

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ func CompareWorkflows(actual *v1alpha1.Workflow, expected *v1alpha1.Workflow) {
8585
gomega.Expect(actual.Spec.Priority).To(gomega.Equal(expected.Spec.Priority), "Priority is not same")
8686
gomega.Expect(actual.Spec.RetryStrategy).To(gomega.Equal(expected.Spec.RetryStrategy), "RetryStrategy is not same")
8787
gomega.Expect(actual.Spec.SchedulerName).To(gomega.Equal(expected.Spec.SchedulerName), "SchedulerName is not same")
88-
gomega.Expect(actual.Spec.SecurityContext).To(gomega.Equal(expected.Spec.SecurityContext), "SecurityContext is not same")
88+
matchPodSecurityContext(actual.Spec.SecurityContext, expected.Spec.SecurityContext, "WorkflowSpec SecurityContext is not same")
8989
gomega.Expect(actual.Spec.Shutdown).To(gomega.Equal(expected.Spec.Shutdown), "Shutdown is not same")
9090
gomega.Expect(actual.Spec.ServiceAccountName).To(gomega.Equal(expected.Spec.ServiceAccountName), "ServiceAccountName is not same")
9191
gomega.Expect(actual.Spec.Suspend).To(gomega.Equal(expected.Spec.Suspend), "Suspend is not same")
@@ -100,7 +100,7 @@ func CompareWorkflows(actual *v1alpha1.Workflow, expected *v1alpha1.Workflow) {
100100
gomega.Expect(actual.Spec.Templates[index].Synchronization).To(gomega.Equal(template.Synchronization), "Synchronization is not same")
101101
gomega.Expect(actual.Spec.Templates[index].Volumes).To(gomega.Equal(template.Volumes), "Volumes is not same")
102102
gomega.Expect(actual.Spec.Templates[index].Suspend).To(gomega.Equal(template.Suspend), "Suspend is not same")
103-
gomega.Expect(actual.Spec.Templates[index].SecurityContext).To(gomega.Equal(template.SecurityContext), "SecurityContext is not same")
103+
matchPodSecurityContext(actual.Spec.Templates[index].SecurityContext, template.SecurityContext, "Template SecurityContext is not same")
104104
gomega.Expect(actual.Spec.Templates[index].SchedulerName).To(gomega.Equal(template.SchedulerName), "SchedulerName is not same")
105105
gomega.Expect(actual.Spec.Templates[index].RetryStrategy).To(gomega.Equal(template.RetryStrategy), "RetryStrategy is not same")
106106
gomega.Expect(actual.Spec.Templates[index].Parallelism).To(gomega.Equal(template.Parallelism), "Parallelism is not same")
@@ -163,7 +163,7 @@ func MatchContainer(actual *v1.Container, expected *v1.Container) {
163163
if expected != nil {
164164
gomega.Expect(actual.Name).To(gomega.Equal(expected.Name), "Container Name is not same")
165165
gomega.Expect(actual.Args).To(gomega.ConsistOf(expected.Args), "Container Args is not same")
166-
gomega.Expect(actual.SecurityContext).To(gomega.Equal(expected.SecurityContext), "Container SecurityContext is not same")
166+
matchContainerSecurityContext(actual.SecurityContext, expected.SecurityContext, "Container SecurityContext is not same")
167167
gomega.Expect(actual.Env).To(gomega.Equal(expected.Env), "Container Env is not same")
168168
gomega.Expect(actual.EnvFrom).To(gomega.Equal(expected.EnvFrom), "Container EnvFrom is not same")
169169
gomega.Expect(actual.Command).To(gomega.Equal(expected.Command), "Container Command is not same")
@@ -197,7 +197,7 @@ func MatchUserContainer(actual *v1alpha1.UserContainer, expected *v1alpha1.UserC
197197
if expected != nil {
198198
gomega.Expect(actual.Name).To(gomega.Equal(expected.Name), "User Container Name is not same")
199199
gomega.Expect(actual.Args).To(gomega.ConsistOf(expected.Args), "User Container Args is not same")
200-
gomega.Expect(actual.SecurityContext).To(gomega.Equal(expected.SecurityContext), "User Container SecurityContext is not same")
200+
matchContainerSecurityContext(actual.SecurityContext, expected.SecurityContext, "User Container SecurityContext is not same")
201201
gomega.Expect(actual.Env).To(gomega.Equal(expected.Env), "User Container Env is not same")
202202
gomega.Expect(actual.EnvFrom).To(gomega.Equal(expected.EnvFrom), "User Container EnvFrom is not same")
203203
gomega.Expect(actual.Command).To(gomega.Equal(expected.Command), "User Container Command is not same")
@@ -266,3 +266,62 @@ func AreStringsSameWithoutOrder(s1, s2 string) bool {
266266
// Compare the sorted slices
267267
return reflect.DeepEqual(r1, r2)
268268
}
269+
270+
func matchPodSecurityContext(actual *v1.PodSecurityContext, expected *v1.PodSecurityContext, msg string) {
271+
if expected == nil {
272+
return
273+
}
274+
gomega.Expect(actual).NotTo(gomega.BeNil(), msg)
275+
if expected.RunAsUser != nil {
276+
gomega.Expect(actual.RunAsUser).To(gomega.Equal(expected.RunAsUser), msg)
277+
}
278+
if expected.RunAsGroup != nil {
279+
gomega.Expect(actual.RunAsGroup).To(gomega.Equal(expected.RunAsGroup), msg)
280+
}
281+
if expected.FSGroup != nil {
282+
gomega.Expect(actual.FSGroup).To(gomega.Equal(expected.FSGroup), msg)
283+
}
284+
if expected.RunAsNonRoot != nil {
285+
gomega.Expect(actual.RunAsNonRoot).To(gomega.Equal(expected.RunAsNonRoot), msg)
286+
}
287+
if expected.SeccompProfile != nil {
288+
gomega.Expect(actual.SeccompProfile).NotTo(gomega.BeNil(), msg)
289+
gomega.Expect(actual.SeccompProfile.Type).To(gomega.Equal(expected.SeccompProfile.Type), msg)
290+
gomega.Expect(actual.SeccompProfile.LocalhostProfile).To(gomega.Equal(expected.SeccompProfile.LocalhostProfile), msg)
291+
}
292+
}
293+
294+
func matchContainerSecurityContext(actual *v1.SecurityContext, expected *v1.SecurityContext, msg string) {
295+
if expected == nil {
296+
return
297+
}
298+
gomega.Expect(actual).NotTo(gomega.BeNil(), msg)
299+
if expected.AllowPrivilegeEscalation != nil {
300+
gomega.Expect(actual.AllowPrivilegeEscalation).To(gomega.Equal(expected.AllowPrivilegeEscalation), msg)
301+
}
302+
if expected.Privileged != nil {
303+
gomega.Expect(actual.Privileged).To(gomega.Equal(expected.Privileged), msg)
304+
}
305+
if expected.ReadOnlyRootFilesystem != nil {
306+
gomega.Expect(actual.ReadOnlyRootFilesystem).To(gomega.Equal(expected.ReadOnlyRootFilesystem), msg)
307+
}
308+
if expected.RunAsNonRoot != nil {
309+
gomega.Expect(actual.RunAsNonRoot).To(gomega.Equal(expected.RunAsNonRoot), msg)
310+
}
311+
if expected.RunAsUser != nil {
312+
gomega.Expect(actual.RunAsUser).To(gomega.Equal(expected.RunAsUser), msg)
313+
}
314+
if expected.RunAsGroup != nil {
315+
gomega.Expect(actual.RunAsGroup).To(gomega.Equal(expected.RunAsGroup), msg)
316+
}
317+
if expected.Capabilities != nil {
318+
gomega.Expect(actual.Capabilities).NotTo(gomega.BeNil(), msg)
319+
gomega.Expect(actual.Capabilities.Drop).To(gomega.Equal(expected.Capabilities.Drop), msg)
320+
gomega.Expect(actual.Capabilities.Add).To(gomega.Equal(expected.Capabilities.Add), msg)
321+
}
322+
if expected.SeccompProfile != nil {
323+
gomega.Expect(actual.SeccompProfile).NotTo(gomega.BeNil(), msg)
324+
gomega.Expect(actual.SeccompProfile.Type).To(gomega.Equal(expected.SeccompProfile.Type), msg)
325+
gomega.Expect(actual.SeccompProfile.LocalhostProfile).To(gomega.Equal(expected.SeccompProfile.LocalhostProfile), msg)
326+
}
327+
}

manifests/gcp_marketplace/chart/kubeflow-pipelines/templates/argo.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -972,8 +972,12 @@ spec:
972972
- ALL
973973
readOnlyRootFilesystem: true
974974
runAsNonRoot: true
975+
seccompProfile:
976+
type: RuntimeDefault
975977
nodeSelector:
976978
kubernetes.io/os: linux
977979
priorityClassName: workflow-controller
978980
securityContext:
979981
runAsNonRoot: true
982+
seccompProfile:
983+
type: RuntimeDefault

manifests/kustomize/base/cache-deployer/cache-deployer-deployment.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ spec:
1717
app: cache-deployer
1818
spec:
1919
securityContext:
20+
runAsNonRoot: true
2021
seccompProfile:
2122
type: RuntimeDefault
2223
containers:
@@ -29,6 +30,8 @@ spec:
2930
capabilities:
3031
drop:
3132
- ALL
33+
seccompProfile:
34+
type: RuntimeDefault
3235
image: ghcr.io/kubeflow/kfp-cache-deployer:dummy
3336
imagePullPolicy: Always
3437
env:

manifests/kustomize/base/cache/cache-deployment.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ spec:
1515
app: cache-server
1616
spec:
1717
securityContext:
18+
runAsNonRoot: true
1819
seccompProfile:
1920
type: RuntimeDefault
2021
containers:
@@ -27,6 +28,8 @@ spec:
2728
capabilities:
2829
drop:
2930
- ALL
31+
seccompProfile:
32+
type: RuntimeDefault
3033
image: ghcr.io/kubeflow/kfp-cache-server:dummy
3134
env:
3235
- name: DEFAULT_CACHE_STALENESS

0 commit comments

Comments
 (0)