Skip to content

Commit 70f68db

Browse files
authored
Merge pull request kubernetes#92856 from saschagrunert/psp-seccomp-ga
Implement PodSecurityPolicy enforcement for seccomp GA
2 parents 60b8693 + 96fb83c commit 70f68db

File tree

6 files changed

+159
-60
lines changed

6 files changed

+159
-60
lines changed

pkg/api/pod/util.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -745,3 +745,58 @@ func setHostnameAsFQDNInUse(podSpec *api.PodSpec) bool {
745745
}
746746
return *podSpec.SetHostnameAsFQDN
747747
}
748+
749+
// SeccompAnnotationForField takes a pod seccomp profile field and returns the
750+
// converted annotation value
751+
func SeccompAnnotationForField(field *api.SeccompProfile) string {
752+
// If only seccomp fields are specified, add the corresponding annotations.
753+
// This ensures that the fields are enforced even if the node version
754+
// trails the API version
755+
switch field.Type {
756+
case api.SeccompProfileTypeUnconfined:
757+
return v1.SeccompProfileNameUnconfined
758+
759+
case api.SeccompProfileTypeRuntimeDefault:
760+
return v1.SeccompProfileRuntimeDefault
761+
762+
case api.SeccompProfileTypeLocalhost:
763+
if field.LocalhostProfile != nil {
764+
return v1.SeccompLocalhostProfileNamePrefix + *field.LocalhostProfile
765+
}
766+
}
767+
768+
// we can only reach this code path if the LocalhostProfile is nil but the
769+
// provided field type is SeccompProfileTypeLocalhost or if an unrecognized
770+
// type is specified
771+
return ""
772+
}
773+
774+
// SeccompFieldForAnnotation takes a pod annotation and returns the converted
775+
// seccomp profile field.
776+
func SeccompFieldForAnnotation(annotation string) *api.SeccompProfile {
777+
// If only seccomp annotations are specified, copy the values into the
778+
// corresponding fields. This ensures that existing applications continue
779+
// to enforce seccomp, and prevents the kubelet from needing to resolve
780+
// annotations & fields.
781+
if annotation == v1.SeccompProfileNameUnconfined {
782+
return &api.SeccompProfile{Type: api.SeccompProfileTypeUnconfined}
783+
}
784+
785+
if annotation == api.SeccompProfileRuntimeDefault || annotation == api.DeprecatedSeccompProfileDockerDefault {
786+
return &api.SeccompProfile{Type: api.SeccompProfileTypeRuntimeDefault}
787+
}
788+
789+
if strings.HasPrefix(annotation, v1.SeccompLocalhostProfileNamePrefix) {
790+
localhostProfile := strings.TrimPrefix(annotation, v1.SeccompLocalhostProfileNamePrefix)
791+
if localhostProfile != "" {
792+
return &api.SeccompProfile{
793+
Type: api.SeccompProfileTypeLocalhost,
794+
LocalhostProfile: &localhostProfile,
795+
}
796+
}
797+
}
798+
799+
// we can only reach this code path if the localhostProfile name has a zero
800+
// length or if the annotation has an unrecognized value
801+
return nil
802+
}

pkg/registry/core/pod/strategy.go

Lines changed: 4 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -587,7 +587,7 @@ func applySeccompVersionSkew(pod *api.Pod) {
587587

588588
// sync field and annotation
589589
if hasField && !hasAnnotation {
590-
newAnnotation := seccompAnnotationForField(field)
590+
newAnnotation := podutil.SeccompAnnotationForField(field)
591591

592592
if newAnnotation != "" {
593593
if pod.Annotations == nil {
@@ -596,7 +596,7 @@ func applySeccompVersionSkew(pod *api.Pod) {
596596
pod.Annotations[v1.SeccompPodAnnotationKey] = newAnnotation
597597
}
598598
} else if hasAnnotation && !hasField {
599-
newField := seccompFieldForAnnotation(annotation)
599+
newField := podutil.SeccompFieldForAnnotation(annotation)
600600

601601
if newField != nil {
602602
if pod.Spec.SecurityContext == nil {
@@ -621,7 +621,7 @@ func applySeccompVersionSkew(pod *api.Pod) {
621621

622622
// sync field and annotation
623623
if hasField && !hasAnnotation {
624-
newAnnotation := seccompAnnotationForField(field)
624+
newAnnotation := podutil.SeccompAnnotationForField(field)
625625

626626
if newAnnotation != "" {
627627
if pod.Annotations == nil {
@@ -630,7 +630,7 @@ func applySeccompVersionSkew(pod *api.Pod) {
630630
pod.Annotations[key] = newAnnotation
631631
}
632632
} else if hasAnnotation && !hasField {
633-
newField := seccompFieldForAnnotation(annotation)
633+
newField := podutil.SeccompFieldForAnnotation(annotation)
634634

635635
if newField != nil {
636636
if ctr.SecurityContext == nil {
@@ -643,58 +643,3 @@ func applySeccompVersionSkew(pod *api.Pod) {
643643
return true
644644
})
645645
}
646-
647-
// seccompFieldForAnnotation takes a pod seccomp profile field and returns the
648-
// converted annotation value
649-
func seccompAnnotationForField(field *api.SeccompProfile) string {
650-
// If only seccomp fields are specified, add the corresponding annotations.
651-
// This ensures that the fields are enforced even if the node version
652-
// trails the API version
653-
switch field.Type {
654-
case api.SeccompProfileTypeUnconfined:
655-
return v1.SeccompProfileNameUnconfined
656-
657-
case api.SeccompProfileTypeRuntimeDefault:
658-
return v1.SeccompProfileRuntimeDefault
659-
660-
case api.SeccompProfileTypeLocalhost:
661-
if field.LocalhostProfile != nil {
662-
return v1.SeccompLocalhostProfileNamePrefix + *field.LocalhostProfile
663-
}
664-
}
665-
666-
// we can only reach this code path if the LocalhostProfile is nil but the
667-
// provided field type is SeccompProfileTypeLocalhost or if an unrecognized
668-
// type is specified
669-
return ""
670-
}
671-
672-
// seccompFieldForAnnotation takes a pod annotation and returns the converted
673-
// seccomp profile field.
674-
func seccompFieldForAnnotation(annotation string) *api.SeccompProfile {
675-
// If only seccomp annotations are specified, copy the values into the
676-
// corresponding fields. This ensures that existing applications continue
677-
// to enforce seccomp, and prevents the kubelet from needing to resolve
678-
// annotations & fields.
679-
if annotation == v1.SeccompProfileNameUnconfined {
680-
return &api.SeccompProfile{Type: api.SeccompProfileTypeUnconfined}
681-
}
682-
683-
if annotation == api.SeccompProfileRuntimeDefault || annotation == api.DeprecatedSeccompProfileDockerDefault {
684-
return &api.SeccompProfile{Type: api.SeccompProfileTypeRuntimeDefault}
685-
}
686-
687-
if strings.HasPrefix(annotation, v1.SeccompLocalhostProfileNamePrefix) {
688-
localhostProfile := strings.TrimPrefix(annotation, v1.SeccompLocalhostProfileNamePrefix)
689-
if localhostProfile != "" {
690-
return &api.SeccompProfile{
691-
Type: api.SeccompProfileTypeLocalhost,
692-
LocalhostProfile: &localhostProfile,
693-
}
694-
}
695-
}
696-
697-
// we can only reach this code path if the localhostProfile name has a zero
698-
// length or if the annotation has an unrecognized value
699-
return nil
700-
}

pkg/security/podsecuritypolicy/seccomp/BUILD

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ go_library(
1111
srcs = ["strategy.go"],
1212
importpath = "k8s.io/kubernetes/pkg/security/podsecuritypolicy/seccomp",
1313
deps = [
14+
"//pkg/api/pod:go_default_library",
1415
"//pkg/apis/core:go_default_library",
1516
"//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
1617
],
@@ -22,6 +23,7 @@ go_test(
2223
embed = [":go_default_library"],
2324
deps = [
2425
"//pkg/apis/core:go_default_library",
26+
"//staging/src/k8s.io/api/core/v1:go_default_library",
2527
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
2628
],
2729
)

pkg/security/podsecuritypolicy/seccomp/strategy.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"strings"
2222

2323
"k8s.io/apimachinery/pkg/util/validation/field"
24+
podutil "k8s.io/kubernetes/pkg/api/pod"
2425
api "k8s.io/kubernetes/pkg/apis/core"
2526
)
2627

@@ -83,6 +84,10 @@ func (s *strategy) Generate(annotations map[string]string, pod *api.Pod) (string
8384
// Profile already set, nothing to do.
8485
return annotations[api.SeccompPodAnnotationKey], nil
8586
}
87+
if pod.Spec.SecurityContext != nil && pod.Spec.SecurityContext.SeccompProfile != nil {
88+
// Profile field already set, translate to annotation
89+
return podutil.SeccompAnnotationForField(pod.Spec.SecurityContext.SeccompProfile), nil
90+
}
8691
return s.defaultProfile, nil
8792
}
8893

@@ -92,6 +97,10 @@ func (s *strategy) ValidatePod(pod *api.Pod) field.ErrorList {
9297
allErrs := field.ErrorList{}
9398
podSpecFieldPath := field.NewPath("pod", "metadata", "annotations").Key(api.SeccompPodAnnotationKey)
9499
podProfile := pod.Annotations[api.SeccompPodAnnotationKey]
100+
// if the annotation is not set, see if the field is set and derive the corresponding annotation value
101+
if len(podProfile) == 0 && pod.Spec.SecurityContext != nil && pod.Spec.SecurityContext.SeccompProfile != nil {
102+
podProfile = podutil.SeccompAnnotationForField(pod.Spec.SecurityContext.SeccompProfile)
103+
}
95104

96105
if !s.allowAnyProfile && len(s.allowedProfiles) == 0 && podProfile != "" {
97106
allErrs = append(allErrs, field.Forbidden(podSpecFieldPath, "seccomp may not be set"))
@@ -141,9 +150,19 @@ func (s *strategy) profileAllowed(profile string) bool {
141150

142151
// profileForContainer returns the container profile if set, otherwise the pod profile.
143152
func profileForContainer(pod *api.Pod, container *api.Container) string {
153+
if container.SecurityContext != nil && container.SecurityContext.SeccompProfile != nil {
154+
// derive the annotation value from the container field
155+
return podutil.SeccompAnnotationForField(container.SecurityContext.SeccompProfile)
156+
}
144157
containerProfile, ok := pod.Annotations[api.SeccompContainerAnnotationKeyPrefix+container.Name]
145158
if ok {
159+
// return the existing container annotation
146160
return containerProfile
147161
}
162+
if pod.Spec.SecurityContext != nil && pod.Spec.SecurityContext.SeccompProfile != nil {
163+
// derive the annotation value from the pod field
164+
return podutil.SeccompAnnotationForField(pod.Spec.SecurityContext.SeccompProfile)
165+
}
166+
// return the existing pod annotation
148167
return pod.Annotations[api.SeccompPodAnnotationKey]
149168
}

pkg/security/podsecuritypolicy/seccomp/strategy_test.go

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"strings"
2222
"testing"
2323

24+
"k8s.io/api/core/v1"
2425
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2526
api "k8s.io/kubernetes/pkg/apis/core"
2627
)
@@ -41,6 +42,9 @@ var (
4142
allowSpecific = map[string]string{
4243
AllowedProfilesAnnotationKey: "foo",
4344
}
45+
allowSpecificLocalhost = map[string]string{
46+
AllowedProfilesAnnotationKey: v1.SeccompLocalhostProfileNamePrefix + "foo",
47+
}
4448
)
4549

4650
func TestNewStrategy(t *testing.T) {
@@ -102,9 +106,11 @@ func TestNewStrategy(t *testing.T) {
102106
}
103107

104108
func TestGenerate(t *testing.T) {
109+
bar := "bar"
105110
tests := map[string]struct {
106111
pspAnnotations map[string]string
107112
podAnnotations map[string]string
113+
seccompProfile *api.SeccompProfile
108114
expectedProfile string
109115
}{
110116
"no seccomp, no pod annotations": {
@@ -143,10 +149,25 @@ func TestGenerate(t *testing.T) {
143149
},
144150
expectedProfile: "bar",
145151
},
152+
"seccomp with default, pod field": {
153+
pspAnnotations: allowAnyDefault,
154+
seccompProfile: &api.SeccompProfile{
155+
Type: api.SeccompProfileTypeLocalhost,
156+
LocalhostProfile: &bar,
157+
},
158+
expectedProfile: "localhost/bar",
159+
},
146160
}
147161
for k, v := range tests {
148162
s := NewStrategy(v.pspAnnotations)
149-
actual, err := s.Generate(v.podAnnotations, nil)
163+
actual, err := s.Generate(v.podAnnotations, &api.Pod{
164+
Spec: api.PodSpec{
165+
SecurityContext: &api.PodSecurityContext{
166+
SeccompProfile: v.seccompProfile,
167+
},
168+
},
169+
})
170+
150171
if err != nil {
151172
t.Errorf("%s received error during generation %#v", k, err)
152173
continue
@@ -158,9 +179,11 @@ func TestGenerate(t *testing.T) {
158179
}
159180

160181
func TestValidatePod(t *testing.T) {
182+
foo := "foo"
161183
tests := map[string]struct {
162184
pspAnnotations map[string]string
163185
podAnnotations map[string]string
186+
seccompProfile *api.SeccompProfile
164187
expectedError string
165188
}{
166189
"no pod annotations, required profiles": {
@@ -206,12 +229,44 @@ func TestValidatePod(t *testing.T) {
206229
podAnnotations: nil,
207230
expectedError: "",
208231
},
232+
"valid pod annotations and field, required profiles": {
233+
pspAnnotations: allowSpecific,
234+
podAnnotations: map[string]string{
235+
api.SeccompPodAnnotationKey: "foo",
236+
},
237+
seccompProfile: &api.SeccompProfile{
238+
Type: api.SeccompProfileTypeLocalhost,
239+
LocalhostProfile: &foo,
240+
},
241+
expectedError: "",
242+
},
243+
"valid pod field and no annotation, required profiles": {
244+
pspAnnotations: allowSpecific,
245+
seccompProfile: &api.SeccompProfile{
246+
Type: api.SeccompProfileTypeLocalhost,
247+
LocalhostProfile: &foo,
248+
},
249+
expectedError: "Forbidden: localhost/foo is not an allowed seccomp profile. Valid values are foo",
250+
},
251+
"valid pod field and no annotation, required profiles (localhost)": {
252+
pspAnnotations: allowSpecificLocalhost,
253+
seccompProfile: &api.SeccompProfile{
254+
Type: api.SeccompProfileTypeLocalhost,
255+
LocalhostProfile: &foo,
256+
},
257+
expectedError: "",
258+
},
209259
}
210260
for k, v := range tests {
211261
pod := &api.Pod{
212262
ObjectMeta: metav1.ObjectMeta{
213263
Annotations: v.podAnnotations,
214264
},
265+
Spec: api.PodSpec{
266+
SecurityContext: &api.PodSecurityContext{
267+
SeccompProfile: v.seccompProfile,
268+
},
269+
},
215270
}
216271
s := NewStrategy(v.pspAnnotations)
217272
errs := s.ValidatePod(pod)
@@ -231,9 +286,12 @@ func TestValidatePod(t *testing.T) {
231286
}
232287

233288
func TestValidateContainer(t *testing.T) {
289+
foo := "foo"
290+
bar := "bar"
234291
tests := map[string]struct {
235292
pspAnnotations map[string]string
236293
podAnnotations map[string]string
294+
seccompProfile *api.SeccompProfile
237295
expectedError string
238296
}{
239297
"no pod annotations, required profiles": {
@@ -293,6 +351,22 @@ func TestValidateContainer(t *testing.T) {
293351
},
294352
expectedError: "Forbidden: bar is not an allowed seccomp profile. Valid values are foo",
295353
},
354+
"valid container field and no annotation, required profiles": {
355+
pspAnnotations: allowSpecificLocalhost,
356+
seccompProfile: &api.SeccompProfile{
357+
Type: api.SeccompProfileTypeLocalhost,
358+
LocalhostProfile: &foo,
359+
},
360+
expectedError: "",
361+
},
362+
"invalid container field and no annotation, required profiles": {
363+
pspAnnotations: allowSpecificLocalhost,
364+
seccompProfile: &api.SeccompProfile{
365+
Type: api.SeccompProfileTypeLocalhost,
366+
LocalhostProfile: &bar,
367+
},
368+
expectedError: "Forbidden: localhost/bar is not an allowed seccomp profile. Valid values are localhost/foo",
369+
},
296370
}
297371
for k, v := range tests {
298372
pod := &api.Pod{
@@ -302,6 +376,9 @@ func TestValidateContainer(t *testing.T) {
302376
}
303377
container := &api.Container{
304378
Name: "container",
379+
SecurityContext: &api.SecurityContext{
380+
SeccompProfile: v.seccompProfile,
381+
},
305382
}
306383

307384
s := NewStrategy(v.pspAnnotations)

test/e2e/framework/.import-restrictions

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ rules:
66
- k8s.io/kubernetes/pkg/api/v1/pod
77
- k8s.io/kubernetes/pkg/api/v1/resource
88
- k8s.io/kubernetes/pkg/api/v1/service
9+
- k8s.io/kubernetes/pkg/api/pod
910
- k8s.io/kubernetes/pkg/apis/apps
1011
- k8s.io/kubernetes/pkg/apis/apps/validation
1112
- k8s.io/kubernetes/pkg/apis/autoscaling

0 commit comments

Comments
 (0)