diff --git a/hack/.linted_packages b/hack/.linted_packages index 0460215127..5aebef107d 100644 --- a/hack/.linted_packages +++ b/hack/.linted_packages @@ -206,4 +206,5 @@ pkg/util/maps pkg/volume/quobyte test/integration/discoverysummarizer test/integration/examples -test/integration/federation \ No newline at end of file +test/integration/federation +pkg/security/podsecuritypolicy/apparmor diff --git a/pkg/apis/extensions/validation/validation.go b/pkg/apis/extensions/validation/validation.go index a15fb523b2..1c7ef92466 100644 --- a/pkg/apis/extensions/validation/validation.go +++ b/pkg/apis/extensions/validation/validation.go @@ -30,6 +30,7 @@ import ( apivalidation "k8s.io/kubernetes/pkg/api/validation" "k8s.io/kubernetes/pkg/apis/extensions" "k8s.io/kubernetes/pkg/labels" + "k8s.io/kubernetes/pkg/security/apparmor" psputil "k8s.io/kubernetes/pkg/security/podsecuritypolicy/util" "k8s.io/kubernetes/pkg/util/intstr" "k8s.io/kubernetes/pkg/util/sets" @@ -552,6 +553,7 @@ var ValidatePodSecurityPolicyName = apivalidation.NameIsDNSSubdomain func ValidatePodSecurityPolicy(psp *extensions.PodSecurityPolicy) field.ErrorList { allErrs := field.ErrorList{} allErrs = append(allErrs, apivalidation.ValidateObjectMeta(&psp.ObjectMeta, false, ValidatePodSecurityPolicyName, field.NewPath("metadata"))...) + allErrs = append(allErrs, ValidatePodSecurityPolicySpecificAnnotations(psp.Annotations, field.NewPath("metadata").Child("annotations"))...) allErrs = append(allErrs, ValidatePodSecurityPolicySpec(&psp.Spec, field.NewPath("spec"))...) return allErrs } @@ -570,6 +572,23 @@ func ValidatePodSecurityPolicySpec(spec *extensions.PodSecurityPolicySpec, fldPa return allErrs } +func ValidatePodSecurityPolicySpecificAnnotations(annotations map[string]string, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + if p := annotations[apparmor.DefaultProfileAnnotationKey]; p != "" { + if err := apparmor.ValidateProfileFormat(p); err != nil { + allErrs = append(allErrs, field.Invalid(fldPath.Key(apparmor.DefaultProfileAnnotationKey), p, err.Error())) + } + } + if allowed := annotations[apparmor.AllowedProfilesAnnotationKey]; allowed != "" { + for _, p := range strings.Split(allowed, ",") { + if err := apparmor.ValidateProfileFormat(p); err != nil { + allErrs = append(allErrs, field.Invalid(fldPath.Key(apparmor.AllowedProfilesAnnotationKey), allowed, err.Error())) + } + } + } + return allErrs +} + // validatePSPSELinux validates the SELinux fields of PodSecurityPolicy. func validatePSPSELinux(fldPath *field.Path, seLinux *extensions.SELinuxStrategyOptions) field.ErrorList { allErrs := field.ErrorList{} diff --git a/pkg/apis/extensions/validation/validation_test.go b/pkg/apis/extensions/validation/validation_test.go index 33d742d942..93e6f9b303 100644 --- a/pkg/apis/extensions/validation/validation_test.go +++ b/pkg/apis/extensions/validation/validation_test.go @@ -24,6 +24,7 @@ import ( "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/security/apparmor" psputil "k8s.io/kubernetes/pkg/security/podsecuritypolicy/util" "k8s.io/kubernetes/pkg/util/intstr" "k8s.io/kubernetes/pkg/util/validation/field" @@ -1510,7 +1511,9 @@ func TestValidateReplicaSet(t *testing.T) { func TestValidatePodSecurityPolicy(t *testing.T) { validPSP := func() *extensions.PodSecurityPolicy { return &extensions.PodSecurityPolicy{ - ObjectMeta: api.ObjectMeta{Name: "foo"}, + ObjectMeta: api.ObjectMeta{ + Name: "foo", + }, Spec: extensions.PodSecurityPolicySpec{ SELinux: extensions.SELinuxStrategyOptions{ Rule: extensions.SELinuxStrategyRunAsAny, @@ -1584,6 +1587,15 @@ func TestValidatePodSecurityPolicy(t *testing.T) { allowedCapListedInRequiredDrop.Spec.RequiredDropCapabilities = []api.Capability{"foo"} allowedCapListedInRequiredDrop.Spec.AllowedCapabilities = []api.Capability{"foo"} + invalidAppArmorDefault := validPSP() + invalidAppArmorDefault.Annotations = map[string]string{ + apparmor.DefaultProfileAnnotationKey: "not-good", + } + invalidAppArmorAllowed := validPSP() + invalidAppArmorAllowed.Annotations = map[string]string{ + apparmor.AllowedProfilesAnnotationKey: apparmor.ProfileRuntimeDefault + ",not-good", + } + errorCases := map[string]struct { psp *extensions.PodSecurityPolicy errorType field.ErrorType @@ -1664,6 +1676,16 @@ func TestValidatePodSecurityPolicy(t *testing.T) { errorType: field.ErrorTypeInvalid, errorDetail: "capability is listed in allowedCapabilities and requiredDropCapabilities", }, + "invalid AppArmor default profile": { + psp: invalidAppArmorDefault, + errorType: field.ErrorTypeInvalid, + errorDetail: "invalid AppArmor profile name: \"not-good\"", + }, + "invalid AppArmor allowed profile": { + psp: invalidAppArmorAllowed, + errorType: field.ErrorTypeInvalid, + errorDetail: "invalid AppArmor profile name: \"not-good\"", + }, } for k, v := range errorCases { @@ -1700,6 +1722,12 @@ func TestValidatePodSecurityPolicy(t *testing.T) { caseInsensitiveAllowedDrop.Spec.RequiredDropCapabilities = []api.Capability{"FOO"} caseInsensitiveAllowedDrop.Spec.AllowedCapabilities = []api.Capability{"foo"} + validAppArmor := validPSP() + validAppArmor.Annotations = map[string]string{ + apparmor.DefaultProfileAnnotationKey: apparmor.ProfileRuntimeDefault, + apparmor.AllowedProfilesAnnotationKey: apparmor.ProfileRuntimeDefault + "," + apparmor.ProfileNamePrefix + "foo", + } + successCases := map[string]struct { psp *extensions.PodSecurityPolicy }{ @@ -1718,6 +1746,9 @@ func TestValidatePodSecurityPolicy(t *testing.T) { "comparison for allowed -> drop is case sensitive": { psp: caseInsensitiveAllowedDrop, }, + "valid AppArmor annotations": { + psp: validAppArmor, + }, } for k, v := range successCases { diff --git a/pkg/security/apparmor/helpers.go b/pkg/security/apparmor/helpers.go index 80695976b7..4546aea4da 100644 --- a/pkg/security/apparmor/helpers.go +++ b/pkg/security/apparmor/helpers.go @@ -26,6 +26,10 @@ import ( const ( // The prefix to an annotation key specifying a container profile. ContainerAnnotationKeyPrefix = "container.apparmor.security.alpha.kubernetes.io/" + // The annotation key specifying the default AppArmor profile. + DefaultProfileAnnotationKey = "apparmor.security.alpha.kubernetes.io/defaultProfileName" + // The annotation key specifying the allowed AppArmor profiles. + AllowedProfilesAnnotationKey = "apparmor.security.alpha.kubernetes.io/allowedProfileNames" // The profile specifying the runtime default. ProfileRuntimeDefault = "runtime/default" @@ -47,3 +51,12 @@ func isRequired(pod *api.Pod) bool { func GetProfileName(pod *api.Pod, containerName string) string { return pod.Annotations[ContainerAnnotationKeyPrefix+containerName] } + +// Sets the name of the profile to use with the container. +func SetProfileName(pod *api.Pod, containerName, profileName string) error { + if pod.Annotations == nil { + pod.Annotations = map[string]string{} + } + pod.Annotations[ContainerAnnotationKeyPrefix+containerName] = profileName + return nil +} diff --git a/pkg/security/podsecuritypolicy/apparmor/strategy.go b/pkg/security/podsecuritypolicy/apparmor/strategy.go new file mode 100644 index 0000000000..2fa8bff687 --- /dev/null +++ b/pkg/security/podsecuritypolicy/apparmor/strategy.go @@ -0,0 +1,110 @@ +/* +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 apparmor + +import ( + "fmt" + "strings" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/security/apparmor" + "k8s.io/kubernetes/pkg/util/maps" + "k8s.io/kubernetes/pkg/util/validation/field" +) + +// Strategy defines the interface for all AppArmor constraint strategies. +type Strategy interface { + // Generate updates the annotations based on constraint rules. The updates are applied to a copy + // of the annotations, and returned. + Generate(annotations map[string]string, container *api.Container) (map[string]string, error) + // Validate ensures that the specified values fall within the range of the strategy. + Validate(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 +} + +var _ Strategy = &strategy{} + +// NewStrategy creates a new strategy that enforces AppArmor profile constraints. +func NewStrategy(pspAnnotations map[string]string) Strategy { + var allowedProfiles map[string]bool + if allowed, ok := pspAnnotations[apparmor.AllowedProfilesAnnotationKey]; ok { + profiles := strings.Split(allowed, ",") + allowedProfiles = make(map[string]bool, len(profiles)) + for _, p := range profiles { + allowedProfiles[p] = true + } + } + return &strategy{ + defaultProfile: pspAnnotations[apparmor.DefaultProfileAnnotationKey], + allowedProfiles: allowedProfiles, + allowedProfilesString: pspAnnotations[apparmor.AllowedProfilesAnnotationKey], + } +} + +func (s *strategy) Generate(annotations map[string]string, container *api.Container) (map[string]string, error) { + copy := maps.CopySS(annotations) + + if annotations[apparmor.ContainerAnnotationKeyPrefix+container.Name] != "" { + // Profile already set, nothing to do. + return copy, nil + } + + if s.defaultProfile == "" { + // No default set. + return copy, nil + } + + if copy == nil { + copy = map[string]string{} + } + // Add the default profile. + copy[apparmor.ContainerAnnotationKeyPrefix+container.Name] = s.defaultProfile + + return copy, nil +} + +func (s *strategy) Validate(pod *api.Pod, container *api.Container) field.ErrorList { + if s.allowedProfiles == nil { + // Unrestricted: allow all. + return nil + } + + allErrs := field.ErrorList{} + fieldPath := field.NewPath("pod", "metadata", "annotations").Key(apparmor.ContainerAnnotationKeyPrefix + container.Name) + + profile := apparmor.GetProfileName(pod, container.Name) + if profile == "" { + if len(s.allowedProfiles) > 0 { + allErrs = append(allErrs, field.Forbidden(fieldPath, "AppArmor profile must be set")) + return allErrs + } + return nil + } + + if !s.allowedProfiles[profile] { + msg := fmt.Sprintf("%s is not an allowed profile. Allowed values: %q", profile, s.allowedProfilesString) + allErrs = append(allErrs, field.Forbidden(fieldPath, msg)) + } + + return allErrs +} diff --git a/pkg/security/podsecuritypolicy/apparmor/strategy_test.go b/pkg/security/podsecuritypolicy/apparmor/strategy_test.go new file mode 100644 index 0000000000..64e56f60ae --- /dev/null +++ b/pkg/security/podsecuritypolicy/apparmor/strategy_test.go @@ -0,0 +1,173 @@ +/* +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 apparmor + +import ( + "testing" + + "github.com/davecgh/go-spew/spew" + "github.com/stretchr/testify/assert" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/security/apparmor" + "k8s.io/kubernetes/pkg/util/maps" +) + +const ( + containerName = "test-c" +) + +var ( + withoutAppArmor = map[string]string{"foo": "bar"} + withDefault = map[string]string{ + "foo": "bar", + apparmor.ContainerAnnotationKeyPrefix + containerName: apparmor.ProfileRuntimeDefault, + } + withLocal = map[string]string{ + "foo": "bar", + apparmor.ContainerAnnotationKeyPrefix + containerName: apparmor.ProfileNamePrefix + "foo", + } + withDisallowed = map[string]string{ + "foo": "bar", + apparmor.ContainerAnnotationKeyPrefix + containerName: apparmor.ProfileNamePrefix + "bad", + } + + noAppArmor = map[string]string{"foo": "bar"} + unconstrainedWithDefault = map[string]string{ + apparmor.DefaultProfileAnnotationKey: apparmor.ProfileRuntimeDefault, + } + constrained = map[string]string{ + apparmor.AllowedProfilesAnnotationKey: apparmor.ProfileRuntimeDefault + "," + + apparmor.ProfileNamePrefix + "foo", + } + constrainedWithDefault = map[string]string{ + apparmor.DefaultProfileAnnotationKey: apparmor.ProfileRuntimeDefault, + apparmor.AllowedProfilesAnnotationKey: apparmor.ProfileRuntimeDefault + "," + + apparmor.ProfileNamePrefix + "foo", + } + + container = api.Container{ + Name: containerName, + Image: "busybox", + } +) + +func TestGenerate(t *testing.T) { + type testcase struct { + pspAnnotations map[string]string + podAnnotations map[string]string + expected map[string]string + } + tests := []testcase{{ + pspAnnotations: noAppArmor, + podAnnotations: withoutAppArmor, + expected: withoutAppArmor, + }, { + pspAnnotations: unconstrainedWithDefault, + podAnnotations: withoutAppArmor, + expected: withDefault, + }, { + pspAnnotations: constrained, + podAnnotations: withoutAppArmor, + expected: withoutAppArmor, + }, { + pspAnnotations: constrainedWithDefault, + podAnnotations: withoutAppArmor, + expected: withDefault, + }} + + // Add unchanging permutations. + for _, podAnnotations := range []map[string]string{withDefault, withLocal} { + for _, pspAnnotations := range []map[string]string{noAppArmor, unconstrainedWithDefault, constrained, constrainedWithDefault} { + tests = append(tests, testcase{ + pspAnnotations: pspAnnotations, + podAnnotations: podAnnotations, + expected: podAnnotations, + }) + } + } + + for i, test := range tests { + s := NewStrategy(test.pspAnnotations) + msgAndArgs := []interface{}{"testcase[%d]: %s", i, spew.Sdump(test)} + actual, err := s.Generate(test.podAnnotations, &container) + assert.NoError(t, err, msgAndArgs...) + assert.Equal(t, test.expected, actual, msgAndArgs...) + } +} + +func TestValidate(t *testing.T) { + type testcase struct { + pspAnnotations map[string]string + podAnnotations map[string]string + expectErr bool + } + tests := []testcase{} + // Valid combinations + for _, podAnnotations := range []map[string]string{withDefault, withLocal} { + for _, pspAnnotations := range []map[string]string{noAppArmor, unconstrainedWithDefault, constrained, constrainedWithDefault} { + tests = append(tests, testcase{ + pspAnnotations: pspAnnotations, + podAnnotations: podAnnotations, + expectErr: false, + }) + } + } + for _, podAnnotations := range []map[string]string{withoutAppArmor, withDisallowed} { + for _, pspAnnotations := range []map[string]string{noAppArmor, unconstrainedWithDefault} { + tests = append(tests, testcase{ + pspAnnotations: pspAnnotations, + podAnnotations: podAnnotations, + expectErr: false, + }) + } + } + // Invalid combinations + for _, podAnnotations := range []map[string]string{withoutAppArmor, withDisallowed} { + for _, pspAnnotations := range []map[string]string{constrained, constrainedWithDefault} { + tests = append(tests, testcase{ + pspAnnotations: pspAnnotations, + podAnnotations: podAnnotations, + expectErr: true, + }) + } + } + + for i, test := range tests { + s := NewStrategy(test.pspAnnotations) + pod, container := makeTestPod(test.podAnnotations) + msgAndArgs := []interface{}{"testcase[%d]: %s", i, spew.Sdump(test)} + errs := s.Validate(pod, container) + if test.expectErr { + assert.Len(t, errs, 1, msgAndArgs...) + } else { + assert.Len(t, errs, 0, msgAndArgs...) + } + } +} + +func makeTestPod(annotations map[string]string) (*api.Pod, *api.Container) { + return &api.Pod{ + ObjectMeta: api.ObjectMeta{ + Name: "test-pod", + Annotations: maps.CopySS(annotations), + }, + Spec: api.PodSpec{ + Containers: []api.Container{container}, + }, + }, &container +} diff --git a/pkg/security/podsecuritypolicy/factory.go b/pkg/security/podsecuritypolicy/factory.go index bacc43be6f..f055c41987 100644 --- a/pkg/security/podsecuritypolicy/factory.go +++ b/pkg/security/podsecuritypolicy/factory.go @@ -21,6 +21,7 @@ import ( "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/apis/extensions" + "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/selinux" @@ -49,6 +50,11 @@ func (f *simpleStrategyFactory) CreateStrategies(psp *extensions.PodSecurityPoli errs = append(errs, err) } + appArmorStrat, err := createAppArmorStrategy(psp) + if err != nil { + errs = append(errs, err) + } + fsGroupStrat, err := createFSGroupStrategy(&psp.Spec.FSGroup) if err != nil { errs = append(errs, err) @@ -71,6 +77,7 @@ func (f *simpleStrategyFactory) CreateStrategies(psp *extensions.PodSecurityPoli strategies := &ProviderStrategies{ RunAsUserStrategy: userStrat, SELinuxStrategy: seLinuxStrat, + AppArmorStrategy: appArmorStrat, FSGroupStrategy: fsGroupStrat, SupplementalGroupStrategy: supGroupStrat, CapabilitiesStrategy: capStrat, @@ -105,6 +112,11 @@ func createSELinuxStrategy(opts *extensions.SELinuxStrategyOptions) (selinux.SEL } } +// createAppArmorStrategy creates a new AppArmor strategy. +func createAppArmorStrategy(psp *extensions.PodSecurityPolicy) (apparmor.Strategy, error) { + return apparmor.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 fa751a7877..82a6156a3a 100644 --- a/pkg/security/podsecuritypolicy/provider.go +++ b/pkg/security/podsecuritypolicy/provider.go @@ -139,6 +139,11 @@ func (s *simpleProvider) CreateContainerSecurityContext(pod *api.Pod, container sc.SELinuxOptions = seLinux } + annotations, err := s.strategies.AppArmorStrategy.Generate(annotations, container) + if err != nil { + return nil, nil, err + } + if sc.Privileged == nil { priv := false sc.Privileged = &priv @@ -220,6 +225,7 @@ func (s *simpleProvider) ValidateContainerSecurityContext(pod *api.Pod, containe sc := container.SecurityContext 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)...) 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 74a1dde62e..da146b87e3 100644 --- a/pkg/security/podsecuritypolicy/provider_test.go +++ b/pkg/security/podsecuritypolicy/provider_test.go @@ -22,13 +22,18 @@ import ( "strings" "testing" + "github.com/davecgh/go-spew/spew" + "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/security/apparmor" psputil "k8s.io/kubernetes/pkg/security/podsecuritypolicy/util" "k8s.io/kubernetes/pkg/util/diff" "k8s.io/kubernetes/pkg/util/validation/field" ) +const defaultContainerName = "test-c" + func TestCreatePodSecurityContextNonmutating(t *testing.T) { // Create a pod with a security context that needs filling in createPod := func() *api.Pod { @@ -303,6 +308,14 @@ func TestValidateContainerSecurityContextFailures(t *testing.T) { Level: "bar", } + failNilAppArmorPod := defaultPod() + failInvalidAppArmorPod := defaultPod() + apparmor.SetProfileName(failInvalidAppArmorPod, defaultContainerName, apparmor.ProfileNamePrefix+"foo") + failAppArmorPSP := defaultPSP() + failAppArmorPSP.Annotations = map[string]string{ + apparmor.AllowedProfilesAnnotationKey: apparmor.ProfileRuntimeDefault, + } + failPrivPod := defaultPod() var priv bool = true failPrivPod.Spec.Containers[0].SecurityContext.Privileged = &priv @@ -347,6 +360,16 @@ func TestValidateContainerSecurityContextFailures(t *testing.T) { psp: failSELinuxPSP, expectedError: "does not match required level", }, + "failNilAppArmor": { + pod: failNilAppArmorPod, + psp: failAppArmorPSP, + expectedError: "AppArmor profile must be set", + }, + "failInvalidAppArmor": { + pod: failInvalidAppArmorPod, + psp: failAppArmorPSP, + expectedError: "localhost/foo is not an allowed profile. Allowed values: \"runtime/default\"", + }, "failPrivPSP": { pod: failPrivPod, psp: defaultPSP(), @@ -499,6 +522,7 @@ func TestValidateContainerSecurityContextSuccess(t *testing.T) { SecurityContext: &api.PodSecurityContext{}, Containers: []api.Container{ { + Name: defaultContainerName, SecurityContext: &api.SecurityContext{ // expected to be set by defaulting mechanisms Privileged: ¬Priv, @@ -510,7 +534,7 @@ func TestValidateContainerSecurityContextSuccess(t *testing.T) { } } - // fail user strat + // success user strat userPSP := defaultPSP() var uid int64 = 999 userPSP.Spec.RunAsUser = extensions.RunAsUserStrategyOptions{ @@ -520,7 +544,7 @@ func TestValidateContainerSecurityContextSuccess(t *testing.T) { userPod := defaultPod() userPod.Spec.Containers[0].SecurityContext.RunAsUser = &uid - // fail selinux strat + // success selinux strat seLinuxPSP := defaultPSP() seLinuxPSP.Spec.SELinux = extensions.SELinuxStrategyOptions{ Rule: extensions.SELinuxStrategyMustRunAs, @@ -533,6 +557,13 @@ func TestValidateContainerSecurityContextSuccess(t *testing.T) { Level: "foo", } + appArmorPSP := defaultPSP() + appArmorPSP.Annotations = map[string]string{ + apparmor.AllowedProfilesAnnotationKey: apparmor.ProfileRuntimeDefault, + } + appArmorPod := defaultPod() + apparmor.SetProfileName(appArmorPod, defaultContainerName, apparmor.ProfileRuntimeDefault) + privPSP := defaultPSP() privPSP.Spec.Privileged = true privPod := defaultPod() @@ -591,6 +622,10 @@ func TestValidateContainerSecurityContextSuccess(t *testing.T) { pod: seLinuxPod, psp: seLinuxPSP, }, + "pass AppArmor allowed profiles": { + pod: appArmorPod, + psp: appArmorPSP, + }, "pass priv validating PSP": { pod: privPod, psp: privPSP, @@ -632,7 +667,7 @@ func TestValidateContainerSecurityContextSuccess(t *testing.T) { } errs := provider.ValidateContainerSecurityContext(v.pod, &v.pod.Spec.Containers[0], field.NewPath("")) if len(errs) != 0 { - t.Errorf("%s expected validation pass but received errors %v", k, errs) + t.Errorf("%s expected validation pass but received errors %v\n%s", k, errs, spew.Sdump(v.pod.ObjectMeta)) continue } } @@ -748,6 +783,7 @@ func defaultPod() *api.Pod { }, Containers: []api.Container{ { + Name: defaultContainerName, SecurityContext: &api.SecurityContext{ // expected to be set by defaulting mechanisms Privileged: ¬Priv, diff --git a/pkg/security/podsecuritypolicy/types.go b/pkg/security/podsecuritypolicy/types.go index a4850cb93e..7cf9104986 100644 --- a/pkg/security/podsecuritypolicy/types.go +++ b/pkg/security/podsecuritypolicy/types.go @@ -19,6 +19,7 @@ package podsecuritypolicy import ( "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/apis/extensions" + "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/selinux" @@ -58,6 +59,7 @@ type StrategyFactory interface { type ProviderStrategies struct { RunAsUserStrategy user.RunAsUserStrategy SELinuxStrategy selinux.SELinuxStrategy + AppArmorStrategy apparmor.Strategy FSGroupStrategy group.GroupStrategy SupplementalGroupStrategy group.GroupStrategy CapabilitiesStrategy capabilities.Strategy diff --git a/plugin/pkg/admission/security/podsecuritypolicy/admission_test.go b/plugin/pkg/admission/security/podsecuritypolicy/admission_test.go index 27b9a6448c..53fc22ae28 100644 --- a/plugin/pkg/admission/security/podsecuritypolicy/admission_test.go +++ b/plugin/pkg/admission/security/podsecuritypolicy/admission_test.go @@ -22,6 +22,8 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" + kadmission "k8s.io/kubernetes/pkg/admission" kapi "k8s.io/kubernetes/pkg/api" extensions "k8s.io/kubernetes/pkg/apis/extensions" @@ -29,11 +31,14 @@ import ( "k8s.io/kubernetes/pkg/client/cache" clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" clientsetfake "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake" + "k8s.io/kubernetes/pkg/security/apparmor" kpsp "k8s.io/kubernetes/pkg/security/podsecuritypolicy" psputil "k8s.io/kubernetes/pkg/security/podsecuritypolicy/util" diff "k8s.io/kubernetes/pkg/util/diff" ) +const defaultContainerName = "test-c" + func NewTestAdmission(store cache.Store, kclient clientset.Interface) kadmission.Interface { return &podSecurityPolicyPlugin{ Handler: kadmission.NewHandler(kadmission.Create), @@ -610,6 +615,85 @@ func TestAdmitSELinux(t *testing.T) { } } +func TestAdmitAppArmor(t *testing.T) { + createPodWithAppArmor := func(profile string) *kapi.Pod { + pod := goodPod() + apparmor.SetProfileName(pod, defaultContainerName, profile) + return pod + } + + unconstrainedPSP := restrictivePSP() + defaultedPSP := restrictivePSP() + defaultedPSP.Annotations = map[string]string{ + apparmor.DefaultProfileAnnotationKey: apparmor.ProfileRuntimeDefault, + } + appArmorPSP := restrictivePSP() + appArmorPSP.Annotations = map[string]string{ + apparmor.AllowedProfilesAnnotationKey: apparmor.ProfileRuntimeDefault, + } + appArmorDefaultPSP := restrictivePSP() + appArmorDefaultPSP.Annotations = map[string]string{ + apparmor.DefaultProfileAnnotationKey: apparmor.ProfileRuntimeDefault, + apparmor.AllowedProfilesAnnotationKey: apparmor.ProfileRuntimeDefault + "," + apparmor.ProfileNamePrefix + "foo", + } + + tests := map[string]struct { + pod *kapi.Pod + psp *extensions.PodSecurityPolicy + shouldPass bool + expectedProfile string + }{ + "unconstrained with no profile": { + pod: goodPod(), + psp: unconstrainedPSP, + shouldPass: true, + expectedProfile: "", + }, + "unconstrained with profile": { + pod: createPodWithAppArmor(apparmor.ProfileRuntimeDefault), + psp: unconstrainedPSP, + shouldPass: true, + expectedProfile: apparmor.ProfileRuntimeDefault, + }, + "unconstrained with default profile": { + pod: goodPod(), + psp: defaultedPSP, + shouldPass: true, + expectedProfile: apparmor.ProfileRuntimeDefault, + }, + "AppArmor enforced with no profile": { + pod: goodPod(), + psp: appArmorPSP, + shouldPass: false, + }, + "AppArmor enforced with default profile": { + pod: goodPod(), + psp: appArmorDefaultPSP, + shouldPass: true, + expectedProfile: apparmor.ProfileRuntimeDefault, + }, + "AppArmor enforced with good profile": { + pod: createPodWithAppArmor(apparmor.ProfileNamePrefix + "foo"), + psp: appArmorDefaultPSP, + shouldPass: true, + expectedProfile: apparmor.ProfileNamePrefix + "foo", + }, + "AppArmor enforced with local profile": { + pod: createPodWithAppArmor(apparmor.ProfileNamePrefix + "bar"), + psp: appArmorPSP, + shouldPass: false, + }, + } + + for k, v := range tests { + testPSPAdmit(k, []*extensions.PodSecurityPolicy{v.psp}, v.pod, v.shouldPass, v.psp.Name, t) + + if v.shouldPass { + assert.Equal(t, v.expectedProfile, apparmor.GetProfileName(v.pod, defaultContainerName), k) + } + } +} + func TestAdmitRunAsUser(t *testing.T) { createPodWithRunAsUser := func(user int64) *kapi.Pod { pod := goodPod() @@ -1212,6 +1296,7 @@ func goodPod() *kapi.Pod { SecurityContext: &kapi.PodSecurityContext{}, Containers: []kapi.Container{ { + Name: defaultContainerName, SecurityContext: &kapi.SecurityContext{}, }, },