From 49e14744db6ab10da8b02c844ebce57c99980b96 Mon Sep 17 00:00:00 2001 From: pweil- Date: Tue, 23 Aug 2016 12:52:27 -0400 Subject: [PATCH] support seccomp in psp --- pkg/api/validation/validation.go | 6 +- pkg/apis/extensions/validation/validation.go | 9 + .../extensions/validation/validation_test.go | 29 ++ pkg/security/podsecuritypolicy/factory.go | 12 + pkg/security/podsecuritypolicy/provider.go | 14 + .../podsecuritypolicy/provider_test.go | 74 ++++ .../podsecuritypolicy/seccomp/strategy.go | 149 ++++++++ .../seccomp/strategy_test.go | 321 ++++++++++++++++++ pkg/security/podsecuritypolicy/types.go | 2 + .../podsecuritypolicy/admission_test.go | 110 ++++++ 10 files changed, 723 insertions(+), 3 deletions(-) create mode 100644 pkg/security/podsecuritypolicy/seccomp/strategy.go create mode 100644 pkg/security/podsecuritypolicy/seccomp/strategy_test.go diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index 0d86635d278..396f3b4017f 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -2119,7 +2119,7 @@ func ValidateTolerationsInPodAnnotations(annotations map[string]string, fldPath return allErrs } -func validateSeccompProfile(p string, fldPath *field.Path) field.ErrorList { +func ValidateSeccompProfile(p string, fldPath *field.Path) field.ErrorList { if p == "docker/default" { return nil } @@ -2135,11 +2135,11 @@ func validateSeccompProfile(p string, fldPath *field.Path) field.ErrorList { func ValidateSeccompPodAnnotations(annotations map[string]string, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} if p, exists := annotations[api.SeccompPodAnnotationKey]; exists { - allErrs = append(allErrs, validateSeccompProfile(p, fldPath.Child(api.SeccompPodAnnotationKey))...) + allErrs = append(allErrs, ValidateSeccompProfile(p, fldPath.Child(api.SeccompPodAnnotationKey))...) } for k, p := range annotations { if strings.HasPrefix(k, api.SeccompContainerAnnotationKeyPrefix) { - allErrs = append(allErrs, validateSeccompProfile(p, fldPath.Child(k))...) + allErrs = append(allErrs, ValidateSeccompProfile(p, fldPath.Child(k))...) } } diff --git a/pkg/apis/extensions/validation/validation.go b/pkg/apis/extensions/validation/validation.go index da744174a6b..15d1e215616 100644 --- a/pkg/apis/extensions/validation/validation.go +++ b/pkg/apis/extensions/validation/validation.go @@ -31,6 +31,7 @@ import ( "k8s.io/kubernetes/pkg/apis/extensions" "k8s.io/kubernetes/pkg/labels" "k8s.io/kubernetes/pkg/security/apparmor" + "k8s.io/kubernetes/pkg/security/podsecuritypolicy/seccomp" psputil "k8s.io/kubernetes/pkg/security/podsecuritypolicy/util" "k8s.io/kubernetes/pkg/util/intstr" "k8s.io/kubernetes/pkg/util/sets" @@ -600,6 +601,14 @@ func ValidatePodSecurityPolicySpecificAnnotations(annotations map[string]string, allErrs = append(allErrs, validatePodSecurityPolicySysctls(sysctlFldPath, sysctls)...) } + if p := annotations[seccomp.DefaultProfileAnnotationKey]; p != "" { + allErrs = append(allErrs, apivalidation.ValidateSeccompProfile(p, fldPath.Key(seccomp.DefaultProfileAnnotationKey))...) + } + if allowed := annotations[seccomp.AllowedProfilesAnnotationKey]; allowed != "" { + for _, p := range strings.Split(allowed, ",") { + allErrs = append(allErrs, apivalidation.ValidateSeccompProfile(p, fldPath.Key(seccomp.AllowedProfilesAnnotationKey))...) + } + } return allErrs } diff --git a/pkg/apis/extensions/validation/validation_test.go b/pkg/apis/extensions/validation/validation_test.go index 00d2b0c9879..3506433aa6c 100644 --- a/pkg/apis/extensions/validation/validation_test.go +++ b/pkg/apis/extensions/validation/validation_test.go @@ -25,6 +25,7 @@ import ( "k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/apis/extensions" "k8s.io/kubernetes/pkg/security/apparmor" + "k8s.io/kubernetes/pkg/security/podsecuritypolicy/seccomp" psputil "k8s.io/kubernetes/pkg/security/podsecuritypolicy/util" "k8s.io/kubernetes/pkg/util/intstr" "k8s.io/kubernetes/pkg/util/validation/field" @@ -1604,6 +1605,15 @@ func TestValidatePodSecurityPolicy(t *testing.T) { invalidSysctlPattern := validPSP() invalidSysctlPattern.Annotations[extensions.SysctlsPodSecurityPolicyAnnotationKey] = "a.*.b" + invalidSeccompDefault := validPSP() + invalidSeccompDefault.Annotations = map[string]string{ + seccomp.DefaultProfileAnnotationKey: "not-good", + } + invalidSeccompAllowed := validPSP() + invalidSeccompAllowed.Annotations = map[string]string{ + seccomp.AllowedProfilesAnnotationKey: "docker/default,not-good", + } + type testCase struct { psp *extensions.PodSecurityPolicy errorType field.ErrorType @@ -1700,6 +1710,16 @@ func TestValidatePodSecurityPolicy(t *testing.T) { errorType: field.ErrorTypeInvalid, errorDetail: fmt.Sprintf("must have at most 253 characters and match regex %s", SysctlPatternFmt), }, + "invalid seccomp default profile": { + psp: invalidSeccompDefault, + errorType: field.ErrorTypeInvalid, + errorDetail: "must be a valid seccomp profile", + }, + "invalid seccomp allowed profile": { + psp: invalidSeccompAllowed, + errorType: field.ErrorTypeInvalid, + errorDetail: "must be a valid seccomp profile", + }, } for k, v := range errorCases { @@ -1768,6 +1788,12 @@ func TestValidatePodSecurityPolicy(t *testing.T) { withSysctl := validPSP() withSysctl.Annotations[extensions.SysctlsPodSecurityPolicyAnnotationKey] = "net.*" + validSeccomp := validPSP() + validSeccomp.Annotations = map[string]string{ + seccomp.DefaultProfileAnnotationKey: "docker/default", + seccomp.AllowedProfilesAnnotationKey: "docker/default,unconfined,localhost/foo", + } + successCases := map[string]struct { psp *extensions.PodSecurityPolicy }{ @@ -1792,6 +1818,9 @@ func TestValidatePodSecurityPolicy(t *testing.T) { "with network sysctls": { psp: withSysctl, }, + "valid seccomp annotations": { + psp: validSeccomp, + }, } for k, v := range successCases { diff --git a/pkg/security/podsecuritypolicy/factory.go b/pkg/security/podsecuritypolicy/factory.go index 64480b38d18..d6d2ba296d8 100644 --- a/pkg/security/podsecuritypolicy/factory.go +++ b/pkg/security/podsecuritypolicy/factory.go @@ -24,6 +24,7 @@ import ( "k8s.io/kubernetes/pkg/security/podsecuritypolicy/apparmor" "k8s.io/kubernetes/pkg/security/podsecuritypolicy/capabilities" "k8s.io/kubernetes/pkg/security/podsecuritypolicy/group" + "k8s.io/kubernetes/pkg/security/podsecuritypolicy/seccomp" "k8s.io/kubernetes/pkg/security/podsecuritypolicy/selinux" "k8s.io/kubernetes/pkg/security/podsecuritypolicy/sysctl" "k8s.io/kubernetes/pkg/security/podsecuritypolicy/user" @@ -56,6 +57,11 @@ func (f *simpleStrategyFactory) CreateStrategies(psp *extensions.PodSecurityPoli errs = append(errs, err) } + seccompStrat, err := createSeccompStrategy(psp) + if err != nil { + errs = append(errs, err) + } + fsGroupStrat, err := createFSGroupStrategy(&psp.Spec.FSGroup) if err != nil { errs = append(errs, err) @@ -92,6 +98,7 @@ func (f *simpleStrategyFactory) CreateStrategies(psp *extensions.PodSecurityPoli FSGroupStrategy: fsGroupStrat, SupplementalGroupStrategy: supGroupStrat, CapabilitiesStrategy: capStrat, + SeccompStrategy: seccompStrat, SysctlsStrategy: sysctlsStrat, } @@ -129,6 +136,11 @@ func createAppArmorStrategy(psp *extensions.PodSecurityPolicy) (apparmor.Strateg return apparmor.NewStrategy(psp.Annotations), nil } +// createSeccompStrategy creates a new seccomp strategy. +func createSeccompStrategy(psp *extensions.PodSecurityPolicy) (seccomp.Strategy, error) { + return seccomp.NewStrategy(psp.Annotations), nil +} + // createFSGroupStrategy creates a new fsgroup strategy func createFSGroupStrategy(opts *extensions.FSGroupStrategyOptions) (group.GroupStrategy, error) { switch opts.Rule { diff --git a/pkg/security/podsecuritypolicy/provider.go b/pkg/security/podsecuritypolicy/provider.go index cda49838d6a..1d23033ce8f 100644 --- a/pkg/security/podsecuritypolicy/provider.go +++ b/pkg/security/podsecuritypolicy/provider.go @@ -103,6 +103,18 @@ func (s *simpleProvider) CreatePodSecurityContext(pod *api.Pod) (*api.PodSecurit sc.SELinuxOptions = seLinux } + // This is only generated on the pod level. Containers inherit the pod's profile. If the + // container has a specific profile set then it will be caught in the validation step. + seccompProfile, err := s.strategies.SeccompStrategy.Generate(annotations, pod) + if err != nil { + return nil, nil, err + } + if seccompProfile != "" { + if annotations == nil { + annotations = map[string]string{} + } + annotations[api.SeccompPodAnnotationKey] = seccompProfile + } return sc, annotations, nil } @@ -188,6 +200,7 @@ func (s *simpleProvider) ValidatePodSecurityContext(pod *api.Pod, fldPath *field } allErrs = append(allErrs, s.strategies.FSGroupStrategy.Validate(pod, fsGroups)...) allErrs = append(allErrs, s.strategies.SupplementalGroupStrategy.Validate(pod, pod.Spec.SecurityContext.SupplementalGroups)...) + allErrs = append(allErrs, s.strategies.SeccompStrategy.ValidatePod(pod)...) // make a dummy container context to reuse the selinux strategies container := &api.Container{ @@ -247,6 +260,7 @@ func (s *simpleProvider) ValidateContainerSecurityContext(pod *api.Pod, containe allErrs = append(allErrs, s.strategies.RunAsUserStrategy.Validate(pod, container)...) allErrs = append(allErrs, s.strategies.SELinuxStrategy.Validate(pod, container)...) allErrs = append(allErrs, s.strategies.AppArmorStrategy.Validate(pod, container)...) + allErrs = append(allErrs, s.strategies.SeccompStrategy.ValidateContainer(pod, container)...) if !s.psp.Spec.Privileged && *sc.Privileged { allErrs = append(allErrs, field.Invalid(fldPath.Child("privileged"), *sc.Privileged, "Privileged containers are not allowed")) diff --git a/pkg/security/podsecuritypolicy/provider_test.go b/pkg/security/podsecuritypolicy/provider_test.go index c18333ec748..26702032ad6 100644 --- a/pkg/security/podsecuritypolicy/provider_test.go +++ b/pkg/security/podsecuritypolicy/provider_test.go @@ -27,6 +27,7 @@ import ( "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/apis/extensions" "k8s.io/kubernetes/pkg/security/apparmor" + "k8s.io/kubernetes/pkg/security/podsecuritypolicy/seccomp" psputil "k8s.io/kubernetes/pkg/security/podsecuritypolicy/util" "k8s.io/kubernetes/pkg/util/diff" "k8s.io/kubernetes/pkg/util/validation/field" @@ -49,6 +50,10 @@ func TestCreatePodSecurityContextNonmutating(t *testing.T) { return &extensions.PodSecurityPolicy{ ObjectMeta: api.ObjectMeta{ Name: "psp-sa", + Annotations: map[string]string{ + seccomp.AllowedProfilesAnnotationKey: "*", + seccomp.DefaultProfileAnnotationKey: "foo", + }, }, Spec: extensions.PodSecurityPolicySpec{ DefaultAddCapabilities: []api.Capability{"foo"}, @@ -121,6 +126,10 @@ func TestCreateContainerSecurityContextNonmutating(t *testing.T) { return &extensions.PodSecurityPolicy{ ObjectMeta: api.ObjectMeta{ Name: "psp-sa", + Annotations: map[string]string{ + seccomp.AllowedProfilesAnnotationKey: "*", + seccomp.DefaultProfileAnnotationKey: "foo", + }, }, Spec: extensions.PodSecurityPolicySpec{ DefaultAddCapabilities: []api.Capability{"foo"}, @@ -238,6 +247,9 @@ func TestValidatePodSecurityContextFailures(t *testing.T) { failUnsafeSysctlFooPod := defaultPod() failUnsafeSysctlFooPod.Annotations[api.UnsafeSysctlsPodAnnotationKey] = "foo=1" + failSeccompProfilePod := defaultPod() + failSeccompProfilePod.Annotations = map[string]string{api.SeccompPodAnnotationKey: "foo"} + errorCases := map[string]struct { pod *api.Pod psp *extensions.PodSecurityPolicy @@ -313,6 +325,11 @@ func TestValidatePodSecurityContextFailures(t *testing.T) { psp: failOtherSysctlsAllowedPSP, expectedError: "sysctl \"foo\" is not allowed", }, + "failInvalidSeccomp": { + pod: failSeccompProfilePod, + psp: defaultPSP(), + expectedError: "Forbidden: seccomp may not be set", + }, } for k, v := range errorCases { provider, err := NewSimpleProvider(v.psp, "namespace", NewSimpleStrategyFactory()) @@ -382,6 +399,16 @@ func TestValidateContainerSecurityContextFailures(t *testing.T) { readOnlyRootFS := false readOnlyRootFSPodFalse.Spec.Containers[0].SecurityContext.ReadOnlyRootFilesystem = &readOnlyRootFS + failSeccompPod := defaultPod() + failSeccompPod.Annotations = map[string]string{ + api.SeccompContainerAnnotationKeyPrefix + failSeccompPod.Spec.Containers[0].Name: "foo", + } + + failSeccompPodInheritPodAnnotation := defaultPod() + failSeccompPodInheritPodAnnotation.Annotations = map[string]string{ + api.SeccompPodAnnotationKey: "foo", + } + errorCases := map[string]struct { pod *api.Pod psp *extensions.PodSecurityPolicy @@ -432,6 +459,16 @@ func TestValidateContainerSecurityContextFailures(t *testing.T) { psp: readOnlyRootFSPSP, expectedError: "ReadOnlyRootFilesystem must be set to true", }, + "failSeccompContainerAnnotation": { + pod: failSeccompPod, + psp: defaultPSP(), + expectedError: "Forbidden: seccomp may not be set", + }, + "failSeccompContainerPodAnnotation": { + pod: failSeccompPodInheritPodAnnotation, + psp: defaultPSP(), + expectedError: "Forbidden: seccomp may not be set", + }, } for k, v := range errorCases { @@ -512,6 +549,16 @@ func TestValidatePodSecurityContextSuccess(t *testing.T) { unsafeSysctlFooPod := defaultPod() unsafeSysctlFooPod.Annotations[api.UnsafeSysctlsPodAnnotationKey] = "foo=1" + seccompPSP := defaultPSP() + seccompPSP.Annotations = map[string]string{ + seccomp.AllowedProfilesAnnotationKey: "foo", + } + + seccompPod := defaultPod() + seccompPod.Annotations = map[string]string{ + api.SeccompPodAnnotationKey: "foo", + } + errorCases := map[string]struct { pod *api.Pod psp *extensions.PodSecurityPolicy @@ -556,6 +603,10 @@ func TestValidatePodSecurityContextSuccess(t *testing.T) { pod: unsafeSysctlFooPod, psp: defaultPSP(), }, + "pass seccomp validating PSP": { + pod: seccompPod, + psp: seccompPSP, + }, } for k, v := range errorCases { @@ -667,6 +718,21 @@ func TestValidateContainerSecurityContextSuccess(t *testing.T) { readOnlyRootFSTrue := true readOnlyRootFSPodTrue.Spec.Containers[0].SecurityContext.ReadOnlyRootFilesystem = &readOnlyRootFSTrue + seccompPSP := defaultPSP() + seccompPSP.Annotations = map[string]string{ + seccomp.AllowedProfilesAnnotationKey: "foo", + } + + seccompPod := defaultPod() + seccompPod.Annotations = map[string]string{ + api.SeccompContainerAnnotationKeyPrefix + seccompPod.Spec.Containers[0].Name: "foo", + } + + seccompPodInherit := defaultPod() + seccompPodInherit.Annotations = map[string]string{ + api.SeccompPodAnnotationKey: "foo", + } + errorCases := map[string]struct { pod *api.Pod psp *extensions.PodSecurityPolicy @@ -715,6 +781,14 @@ func TestValidateContainerSecurityContextSuccess(t *testing.T) { pod: readOnlyRootFSPodTrue, psp: defaultPSP(), }, + "pass seccomp container annotation": { + pod: seccompPod, + psp: seccompPSP, + }, + "pass seccomp inherit pod annotation": { + pod: seccompPodInherit, + psp: seccompPSP, + }, } for k, v := range errorCases { diff --git a/pkg/security/podsecuritypolicy/seccomp/strategy.go b/pkg/security/podsecuritypolicy/seccomp/strategy.go new file mode 100644 index 00000000000..fd0b32e6573 --- /dev/null +++ b/pkg/security/podsecuritypolicy/seccomp/strategy.go @@ -0,0 +1,149 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package seccomp + +import ( + "fmt" + "strings" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/util/validation/field" +) + +const ( + // AllowAny is the wildcard used to allow any profile. + AllowAny = "*" + // The annotation key specifying the default seccomp profile. + DefaultProfileAnnotationKey = "seccomp.security.alpha.kubernetes.io/defaultProfileName" + // The annotation key specifying the allowed seccomp profiles. + AllowedProfilesAnnotationKey = "seccomp.security.alpha.kubernetes.io/allowedProfileNames" +) + +// Strategy defines the interface for all seccomp constraint strategies. +type Strategy interface { + // Generate returns a profile based on constraint rules. + Generate(annotations map[string]string, pod *api.Pod) (string, error) + // Validate ensures that the specified values fall within the range of the strategy. + ValidatePod(pod *api.Pod) field.ErrorList + // Validate ensures that the specified values fall within the range of the strategy. + ValidateContainer(pod *api.Pod, container *api.Container) field.ErrorList +} + +type strategy struct { + defaultProfile string + allowedProfiles map[string]bool + // For printing error messages (preserves order). + allowedProfilesString string + // does the strategy allow any profile (wildcard) + allowAnyProfile bool +} + +var _ Strategy = &strategy{} + +// NewStrategy creates a new strategy that enforces seccomp profile constraints. +func NewStrategy(pspAnnotations map[string]string) Strategy { + var allowedProfiles map[string]bool + allowAnyProfile := false + if allowed, ok := pspAnnotations[AllowedProfilesAnnotationKey]; ok { + profiles := strings.Split(allowed, ",") + allowedProfiles = make(map[string]bool, len(profiles)) + for _, p := range profiles { + if p == AllowAny { + allowAnyProfile = true + continue + } + allowedProfiles[p] = true + } + } + return &strategy{ + defaultProfile: pspAnnotations[DefaultProfileAnnotationKey], + allowedProfiles: allowedProfiles, + allowedProfilesString: pspAnnotations[AllowedProfilesAnnotationKey], + allowAnyProfile: allowAnyProfile, + } +} + +// Generate returns a profile based on constraint rules. +func (s *strategy) Generate(annotations map[string]string, pod *api.Pod) (string, error) { + if annotations[api.SeccompPodAnnotationKey] != "" { + // Profile already set, nothing to do. + return annotations[api.SeccompPodAnnotationKey], nil + } + return s.defaultProfile, nil +} + +// ValidatePod ensures that the specified values on the pod fall within the range +// of the strategy. +func (s *strategy) ValidatePod(pod *api.Pod) field.ErrorList { + allErrs := field.ErrorList{} + podSpecFieldPath := field.NewPath("pod", "metadata", "annotations").Key(api.SeccompPodAnnotationKey) + podProfile := pod.Annotations[api.SeccompPodAnnotationKey] + + if !s.allowAnyProfile && len(s.allowedProfiles) == 0 && podProfile != "" { + allErrs = append(allErrs, field.Forbidden(podSpecFieldPath, "seccomp may not be set")) + return allErrs + } + + if !s.profileAllowed(podProfile) { + msg := fmt.Sprintf("%s is not an allowed seccomp profile. Valid values are %v", podProfile, s.allowedProfilesString) + allErrs = append(allErrs, field.Forbidden(podSpecFieldPath, msg)) + } + + return allErrs +} + +// ValidateContainer ensures that the specified values on the container fall within +// the range of the strategy. +func (s *strategy) ValidateContainer(pod *api.Pod, container *api.Container) field.ErrorList { + allErrs := field.ErrorList{} + fieldPath := field.NewPath("pod", "metadata", "annotations").Key(api.SeccompContainerAnnotationKeyPrefix + container.Name) + containerProfile := profileForContainer(pod, container) + + if !s.allowAnyProfile && len(s.allowedProfiles) == 0 && containerProfile != "" { + allErrs = append(allErrs, field.Forbidden(fieldPath, "seccomp may not be set")) + return allErrs + } + + if !s.profileAllowed(containerProfile) { + msg := fmt.Sprintf("%s is not an allowed seccomp profile. Valid values are %v", containerProfile, s.allowedProfilesString) + allErrs = append(allErrs, field.Forbidden(fieldPath, msg)) + } + + return allErrs +} + +// profileAllowed checks if profile is in allowedProfiles or if allowedProfiles +// contains the wildcard. +func (s *strategy) profileAllowed(profile string) bool { + // for backwards compatibility and PSPs without a defined list of allowed profiles. + // If a PSP does not have allowedProfiles set then we should allow an empty profile. + // This will mean that the runtime default is used. + if len(s.allowedProfiles) == 0 && profile == "" { + return true + } + + return s.allowAnyProfile || s.allowedProfiles[profile] +} + +// profileForContainer returns the container profile if set, otherwise the pod profile. +func profileForContainer(pod *api.Pod, container *api.Container) string { + containerProfile, ok := pod.Annotations[api.SeccompContainerAnnotationKeyPrefix+container.Name] + if ok { + return containerProfile + } + return pod.Annotations[api.SeccompPodAnnotationKey] +} diff --git a/pkg/security/podsecuritypolicy/seccomp/strategy_test.go b/pkg/security/podsecuritypolicy/seccomp/strategy_test.go new file mode 100644 index 00000000000..6a371f4c05e --- /dev/null +++ b/pkg/security/podsecuritypolicy/seccomp/strategy_test.go @@ -0,0 +1,321 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package seccomp + +import ( + "reflect" + "strings" + "testing" + + "k8s.io/kubernetes/pkg/api" +) + +var ( + withoutSeccomp = map[string]string{"foo": "bar"} + allowAnyNoDefault = map[string]string{ + AllowedProfilesAnnotationKey: "*", + } + allowAnyDefault = map[string]string{ + AllowedProfilesAnnotationKey: "*", + DefaultProfileAnnotationKey: "foo", + } + allowAnyAndSpecificDefault = map[string]string{ + AllowedProfilesAnnotationKey: "*,bar", + DefaultProfileAnnotationKey: "foo", + } + allowSpecific = map[string]string{ + AllowedProfilesAnnotationKey: "foo", + } +) + +func TestNewStrategy(t *testing.T) { + tests := map[string]struct { + annotations map[string]string + expectedAllowedProfilesString string + expectedAllowAny bool + expectedAllowedProfiles map[string]bool + expectedDefaultProfile string + }{ + "no seccomp": { + annotations: withoutSeccomp, + expectedAllowAny: false, + expectedAllowedProfilesString: "", + expectedAllowedProfiles: nil, + expectedDefaultProfile: "", + }, + "allow any, no default": { + annotations: allowAnyNoDefault, + expectedAllowAny: true, + expectedAllowedProfilesString: "*", + expectedAllowedProfiles: map[string]bool{}, + expectedDefaultProfile: "", + }, + "allow any, default": { + annotations: allowAnyDefault, + expectedAllowAny: true, + expectedAllowedProfilesString: "*", + expectedAllowedProfiles: map[string]bool{}, + expectedDefaultProfile: "foo", + }, + "allow any and specific, default": { + annotations: allowAnyAndSpecificDefault, + expectedAllowAny: true, + expectedAllowedProfilesString: "*,bar", + expectedAllowedProfiles: map[string]bool{ + "bar": true, + }, + expectedDefaultProfile: "foo", + }, + } + for k, v := range tests { + strat := NewStrategy(v.annotations) + internalStrat, _ := strat.(*strategy) + + if internalStrat.allowAnyProfile != v.expectedAllowAny { + t.Errorf("%s expected allowAnyProfile to be %t but found %t", k, v.expectedAllowAny, internalStrat.allowAnyProfile) + } + if internalStrat.allowedProfilesString != v.expectedAllowedProfilesString { + t.Errorf("%s expected allowedProfilesString to be %s but found %s", k, v.expectedAllowedProfilesString, internalStrat.allowedProfilesString) + } + if internalStrat.defaultProfile != v.expectedDefaultProfile { + t.Errorf("%s expected defaultProfile to be %s but found %s", k, v.expectedDefaultProfile, internalStrat.defaultProfile) + } + if !reflect.DeepEqual(v.expectedAllowedProfiles, internalStrat.allowedProfiles) { + t.Errorf("%s expected expectedAllowedProfiles to be %#v but found %#v", k, v.expectedAllowedProfiles, internalStrat.allowedProfiles) + } + } +} + +func TestGenerate(t *testing.T) { + tests := map[string]struct { + pspAnnotations map[string]string + podAnnotations map[string]string + expectedProfile string + }{ + "no seccomp, no pod annotations": { + pspAnnotations: withoutSeccomp, + podAnnotations: nil, + expectedProfile: "", + }, + "no seccomp, pod annotations": { + pspAnnotations: withoutSeccomp, + podAnnotations: map[string]string{ + api.SeccompPodAnnotationKey: "foo", + }, + expectedProfile: "foo", + }, + "seccomp with no default, no pod annotations": { + pspAnnotations: allowAnyNoDefault, + podAnnotations: nil, + expectedProfile: "", + }, + "seccomp with no default, pod annotations": { + pspAnnotations: allowAnyNoDefault, + podAnnotations: map[string]string{ + api.SeccompPodAnnotationKey: "foo", + }, + expectedProfile: "foo", + }, + "seccomp with default, no pod annotations": { + pspAnnotations: allowAnyDefault, + podAnnotations: nil, + expectedProfile: "foo", + }, + "seccomp with default, pod annotations": { + pspAnnotations: allowAnyDefault, + podAnnotations: map[string]string{ + api.SeccompPodAnnotationKey: "bar", + }, + expectedProfile: "bar", + }, + } + for k, v := range tests { + strat := NewStrategy(v.pspAnnotations) + actual, err := strat.Generate(v.podAnnotations, nil) + if err != nil { + t.Errorf("%s received error during generation %#v", k, err) + continue + } + if actual != v.expectedProfile { + t.Errorf("%s expected profile %s but received %s", k, v.expectedProfile, actual) + } + } +} + +func TestValidatePod(t *testing.T) { + tests := map[string]struct { + pspAnnotations map[string]string + podAnnotations map[string]string + expectedError string + }{ + "no pod annotations, required profiles": { + pspAnnotations: allowSpecific, + podAnnotations: nil, + expectedError: "Forbidden: is not an allowed seccomp profile. Valid values are foo", + }, + "no pod annotations, no required profiles": { + pspAnnotations: withoutSeccomp, + podAnnotations: nil, + expectedError: "", + }, + "valid pod annotations, required profiles": { + pspAnnotations: allowSpecific, + podAnnotations: map[string]string{ + api.SeccompPodAnnotationKey: "foo", + }, + expectedError: "", + }, + "invalid pod annotations, required profiles": { + pspAnnotations: allowSpecific, + podAnnotations: map[string]string{ + api.SeccompPodAnnotationKey: "bar", + }, + expectedError: "Forbidden: bar is not an allowed seccomp profile. Valid values are foo", + }, + "pod annotations, no required profiles": { + pspAnnotations: withoutSeccomp, + podAnnotations: map[string]string{ + api.SeccompPodAnnotationKey: "foo", + }, + expectedError: "Forbidden: seccomp may not be set", + }, + "pod annotations, allow any": { + pspAnnotations: allowAnyNoDefault, + podAnnotations: map[string]string{ + api.SeccompPodAnnotationKey: "foo", + }, + expectedError: "", + }, + "no pod annotations, allow any": { + pspAnnotations: allowAnyNoDefault, + podAnnotations: nil, + expectedError: "", + }, + } + for k, v := range tests { + pod := &api.Pod{ + ObjectMeta: api.ObjectMeta{ + Annotations: v.podAnnotations, + }, + } + strat := NewStrategy(v.pspAnnotations) + errs := strat.ValidatePod(pod) + if v.expectedError == "" && len(errs) != 0 { + t.Errorf("%s expected no errors but received %#v", k, errs.ToAggregate().Error()) + } + if v.expectedError != "" && len(errs) == 0 { + t.Errorf("%s expected error %s but received none", k, v.expectedError) + } + if v.expectedError != "" && len(errs) > 1 { + t.Errorf("%s received multiple errors: %s", k, errs.ToAggregate().Error()) + } + if v.expectedError != "" && len(errs) == 1 && !strings.Contains(errs.ToAggregate().Error(), v.expectedError) { + t.Errorf("%s expected error %s but received %s", k, v.expectedError, errs.ToAggregate().Error()) + } + } +} + +func TestValidateContainer(t *testing.T) { + tests := map[string]struct { + pspAnnotations map[string]string + podAnnotations map[string]string + expectedError string + }{ + "no pod annotations, required profiles": { + pspAnnotations: allowSpecific, + podAnnotations: nil, + expectedError: "Forbidden: is not an allowed seccomp profile. Valid values are foo", + }, + "no pod annotations, no required profiles": { + pspAnnotations: withoutSeccomp, + podAnnotations: nil, + expectedError: "", + }, + "valid pod annotations, required profiles": { + pspAnnotations: allowSpecific, + podAnnotations: map[string]string{ + api.SeccompContainerAnnotationKeyPrefix + "container": "foo", + }, + expectedError: "", + }, + "invalid pod annotations, required profiles": { + pspAnnotations: allowSpecific, + podAnnotations: map[string]string{ + api.SeccompContainerAnnotationKeyPrefix + "container": "bar", + }, + expectedError: "Forbidden: bar is not an allowed seccomp profile. Valid values are foo", + }, + "pod annotations, no required profiles": { + pspAnnotations: withoutSeccomp, + podAnnotations: map[string]string{ + api.SeccompContainerAnnotationKeyPrefix + "container": "foo", + }, + expectedError: "Forbidden: seccomp may not be set", + }, + "pod annotations, allow any": { + pspAnnotations: allowAnyNoDefault, + podAnnotations: map[string]string{ + api.SeccompContainerAnnotationKeyPrefix + "container": "foo", + }, + expectedError: "", + }, + "no pod annotations, allow any": { + pspAnnotations: allowAnyNoDefault, + podAnnotations: nil, + expectedError: "", + }, + "container inherits valid pod annotation": { + pspAnnotations: allowSpecific, + podAnnotations: map[string]string{ + api.SeccompPodAnnotationKey: "foo", + }, + expectedError: "", + }, + "container inherits invalid pod annotation": { + pspAnnotations: allowSpecific, + podAnnotations: map[string]string{ + api.SeccompPodAnnotationKey: "bar", + }, + expectedError: "Forbidden: bar is not an allowed seccomp profile. Valid values are foo", + }, + } + for k, v := range tests { + pod := &api.Pod{ + ObjectMeta: api.ObjectMeta{ + Annotations: v.podAnnotations, + }, + } + container := &api.Container{ + Name: "container", + } + + strat := NewStrategy(v.pspAnnotations) + errs := strat.ValidateContainer(pod, container) + if v.expectedError == "" && len(errs) != 0 { + t.Errorf("%s expected no errors but received %#v", k, errs.ToAggregate().Error()) + } + if v.expectedError != "" && len(errs) == 0 { + t.Errorf("%s expected error %s but received none", k, v.expectedError) + } + if v.expectedError != "" && len(errs) > 1 { + t.Errorf("%s received multiple errors: %s", k, errs.ToAggregate().Error()) + } + if v.expectedError != "" && len(errs) == 1 && !strings.Contains(errs.ToAggregate().Error(), v.expectedError) { + t.Errorf("%s expected error %s but received %s", k, v.expectedError, errs.ToAggregate().Error()) + } + } +} diff --git a/pkg/security/podsecuritypolicy/types.go b/pkg/security/podsecuritypolicy/types.go index d1d34fd16de..e97d046c5d6 100644 --- a/pkg/security/podsecuritypolicy/types.go +++ b/pkg/security/podsecuritypolicy/types.go @@ -22,6 +22,7 @@ import ( "k8s.io/kubernetes/pkg/security/podsecuritypolicy/apparmor" "k8s.io/kubernetes/pkg/security/podsecuritypolicy/capabilities" "k8s.io/kubernetes/pkg/security/podsecuritypolicy/group" + "k8s.io/kubernetes/pkg/security/podsecuritypolicy/seccomp" "k8s.io/kubernetes/pkg/security/podsecuritypolicy/selinux" "k8s.io/kubernetes/pkg/security/podsecuritypolicy/sysctl" "k8s.io/kubernetes/pkg/security/podsecuritypolicy/user" @@ -65,4 +66,5 @@ type ProviderStrategies struct { SupplementalGroupStrategy group.GroupStrategy CapabilitiesStrategy capabilities.Strategy SysctlsStrategy sysctl.SysctlsStrategy + SeccompStrategy seccomp.Strategy } diff --git a/plugin/pkg/admission/security/podsecuritypolicy/admission_test.go b/plugin/pkg/admission/security/podsecuritypolicy/admission_test.go index 3deb21be623..a4f17a280c7 100644 --- a/plugin/pkg/admission/security/podsecuritypolicy/admission_test.go +++ b/plugin/pkg/admission/security/podsecuritypolicy/admission_test.go @@ -33,6 +33,7 @@ import ( clientsetfake "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake" "k8s.io/kubernetes/pkg/security/apparmor" kpsp "k8s.io/kubernetes/pkg/security/podsecuritypolicy" + "k8s.io/kubernetes/pkg/security/podsecuritypolicy/seccomp" psputil "k8s.io/kubernetes/pkg/security/podsecuritypolicy/util" "k8s.io/kubernetes/pkg/util/diff" ) @@ -55,6 +56,115 @@ func useInitContainers(pod *kapi.Pod) *kapi.Pod { return pod } +func TestAdmitSeccomp(t *testing.T) { + containerName := "container" + tests := map[string]struct { + pspAnnotations map[string]string + podAnnotations map[string]string + shouldAdmit bool + }{ + "no seccomp, no pod annotations": { + pspAnnotations: nil, + podAnnotations: nil, + shouldAdmit: true, + }, + "no seccomp, pod annotations": { + pspAnnotations: nil, + podAnnotations: map[string]string{ + kapi.SeccompPodAnnotationKey: "foo", + }, + shouldAdmit: false, + }, + "no seccomp, container annotations": { + pspAnnotations: nil, + podAnnotations: map[string]string{ + kapi.SeccompContainerAnnotationKeyPrefix + containerName: "foo", + }, + shouldAdmit: false, + }, + "seccomp, allow any no pod annotation": { + pspAnnotations: map[string]string{ + seccomp.AllowedProfilesAnnotationKey: seccomp.AllowAny, + }, + podAnnotations: nil, + shouldAdmit: true, + }, + "seccomp, allow any pod annotation": { + pspAnnotations: map[string]string{ + seccomp.AllowedProfilesAnnotationKey: seccomp.AllowAny, + }, + podAnnotations: map[string]string{ + kapi.SeccompPodAnnotationKey: "foo", + }, + shouldAdmit: true, + }, + "seccomp, allow any container annotation": { + pspAnnotations: map[string]string{ + seccomp.AllowedProfilesAnnotationKey: seccomp.AllowAny, + }, + podAnnotations: map[string]string{ + kapi.SeccompContainerAnnotationKeyPrefix + containerName: "foo", + }, + shouldAdmit: true, + }, + "seccomp, allow specific pod annotation failure": { + pspAnnotations: map[string]string{ + seccomp.AllowedProfilesAnnotationKey: "foo", + }, + podAnnotations: map[string]string{ + kapi.SeccompPodAnnotationKey: "bar", + }, + shouldAdmit: false, + }, + "seccomp, allow specific container annotation failure": { + pspAnnotations: map[string]string{ + // provide a default so we don't have to give the pod annotation + seccomp.DefaultProfileAnnotationKey: "foo", + seccomp.AllowedProfilesAnnotationKey: "foo", + }, + podAnnotations: map[string]string{ + kapi.SeccompContainerAnnotationKeyPrefix + containerName: "bar", + }, + shouldAdmit: false, + }, + "seccomp, allow specific pod annotation pass": { + pspAnnotations: map[string]string{ + seccomp.AllowedProfilesAnnotationKey: "foo", + }, + podAnnotations: map[string]string{ + kapi.SeccompPodAnnotationKey: "foo", + }, + shouldAdmit: true, + }, + "seccomp, allow specific container annotation pass": { + pspAnnotations: map[string]string{ + // provide a default so we don't have to give the pod annotation + seccomp.DefaultProfileAnnotationKey: "foo", + seccomp.AllowedProfilesAnnotationKey: "foo,bar", + }, + podAnnotations: map[string]string{ + kapi.SeccompContainerAnnotationKeyPrefix + containerName: "bar", + }, + shouldAdmit: true, + }, + } + for k, v := range tests { + psp := restrictivePSP() + psp.Annotations = v.pspAnnotations + pod := &kapi.Pod{ + ObjectMeta: kapi.ObjectMeta{ + Annotations: v.podAnnotations, + }, + Spec: kapi.PodSpec{ + Containers: []kapi.Container{ + {Name: containerName}, + }, + }, + } + testPSPAdmit(k, []*extensions.PodSecurityPolicy{psp}, pod, v.shouldAdmit, psp.Name, t) + } +} + func TestAdmitPrivileged(t *testing.T) { createPodWithPriv := func(priv bool) *kapi.Pod { pod := goodPod()