From ed36baed20c1070974e2fc8e8561e3e264d6111f Mon Sep 17 00:00:00 2001 From: "Dr. Stefan Schimanski" Date: Fri, 19 Aug 2016 10:33:56 +0200 Subject: [PATCH] Add sysctl PodSecurityPolicy support --- pkg/apis/extensions/helpers.go | 37 ++++ pkg/apis/extensions/helpers_test.go | 62 +++++++ pkg/apis/extensions/types.go | 7 + pkg/apis/extensions/validation/validation.go | 41 +++++ .../extensions/validation/validation_test.go | 69 +++++++- pkg/security/podsecuritypolicy/factory.go | 20 +++ pkg/security/podsecuritypolicy/provider.go | 2 + .../sysctl/mustmatchpatterns.go | 92 ++++++++++ .../sysctl/mustmatchpatterns_test.go | 106 ++++++++++++ .../podsecuritypolicy/sysctl/types.go | 28 +++ pkg/security/podsecuritypolicy/types.go | 2 + .../podsecuritypolicy/admission_test.go | 161 +++++++++++++++++- 12 files changed, 620 insertions(+), 7 deletions(-) create mode 100644 pkg/apis/extensions/helpers.go create mode 100644 pkg/apis/extensions/helpers_test.go create mode 100644 pkg/security/podsecuritypolicy/sysctl/mustmatchpatterns.go create mode 100644 pkg/security/podsecuritypolicy/sysctl/mustmatchpatterns_test.go create mode 100644 pkg/security/podsecuritypolicy/sysctl/types.go diff --git a/pkg/apis/extensions/helpers.go b/pkg/apis/extensions/helpers.go new file mode 100644 index 00000000000..27d3e23add1 --- /dev/null +++ b/pkg/apis/extensions/helpers.go @@ -0,0 +1,37 @@ +/* +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 extensions + +import ( + "strings" +) + +// SysctlsFromPodSecurityPolicyAnnotation parses an annotation value of the key +// SysctlsSecurityPolocyAnnotationKey into a slice of sysctls. An empty slice +// is returned if annotation is the empty string. +func SysctlsFromPodSecurityPolicyAnnotation(annotation string) ([]string, error) { + if len(annotation) == 0 { + return []string{}, nil + } + + return strings.Split(annotation, ","), nil +} + +// PodAnnotationsFromSysctls creates an annotation value for a slice of Sysctls. +func PodAnnotationsFromSysctls(sysctls []string) string { + return strings.Join(sysctls, ",") +} diff --git a/pkg/apis/extensions/helpers_test.go b/pkg/apis/extensions/helpers_test.go new file mode 100644 index 00000000000..29ae139ec96 --- /dev/null +++ b/pkg/apis/extensions/helpers_test.go @@ -0,0 +1,62 @@ +/* +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 extensions + +import ( + "reflect" + "testing" +) + +func TestPodAnnotationsFromSysctls(t *testing.T) { + type Test struct { + sysctls []string + expectedValue string + } + for _, test := range []Test{ + {sysctls: []string{"a.b"}, expectedValue: "a.b"}, + {sysctls: []string{"a.b", "c.d"}, expectedValue: "a.b,c.d"}, + {sysctls: []string{"a.b", "a.b"}, expectedValue: "a.b,a.b"}, + {sysctls: []string{}, expectedValue: ""}, + {sysctls: nil, expectedValue: ""}, + } { + a := PodAnnotationsFromSysctls(test.sysctls) + if a != test.expectedValue { + t.Errorf("wrong value for %v: got=%q wanted=%q", test.sysctls, a, test.expectedValue) + } + } +} + +func TestSysctlsFromPodSecurityPolicyAnnotation(t *testing.T) { + type Test struct { + expectedValue []string + annotation string + } + for _, test := range []Test{ + {annotation: "a.b", expectedValue: []string{"a.b"}}, + {annotation: "a.b,c.d", expectedValue: []string{"a.b", "c.d"}}, + {annotation: "a.b,a.b", expectedValue: []string{"a.b", "a.b"}}, + {annotation: "", expectedValue: []string{}}, + } { + sysctls, err := SysctlsFromPodSecurityPolicyAnnotation(test.annotation) + if err != nil { + t.Errorf("error for %q: %v", test.annotation, err) + } + if !reflect.DeepEqual(sysctls, test.expectedValue) { + t.Errorf("wrong value for %q: got=%v wanted=%v", test.annotation, sysctls, test.expectedValue) + } + } +} diff --git a/pkg/apis/extensions/types.go b/pkg/apis/extensions/types.go index 6543cdfba3f..f98a10ebdd5 100644 --- a/pkg/apis/extensions/types.go +++ b/pkg/apis/extensions/types.go @@ -35,6 +35,13 @@ import ( "k8s.io/kubernetes/pkg/util/intstr" ) +const ( + // SysctlsPodSecurityPolicyAnnotationKey represents the key of a whitelist of + // allowed safe and unsafe sysctls in a pod spec. It's a comma-separated list of plain sysctl + // names or sysctl patterns (which end in *). The string "*" matches all sysctls. + SysctlsPodSecurityPolicyAnnotationKey string = "security.alpha.kubernetes.io/sysctls" +) + // describes the attributes of a scale subresource type ScaleSpec struct { // desired number of instances for the scaled object. diff --git a/pkg/apis/extensions/validation/validation.go b/pkg/apis/extensions/validation/validation.go index 1c7ef924662..bb2526d6679 100644 --- a/pkg/apis/extensions/validation/validation.go +++ b/pkg/apis/extensions/validation/validation.go @@ -574,6 +574,7 @@ func ValidatePodSecurityPolicySpec(spec *extensions.PodSecurityPolicySpec, fldPa 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())) @@ -586,6 +587,16 @@ func ValidatePodSecurityPolicySpecificAnnotations(annotations map[string]string, } } } + + sysctlAnnotation := annotations[extensions.SysctlsPodSecurityPolicyAnnotationKey] + sysctlFldPath := fldPath.Key(extensions.SysctlsPodSecurityPolicyAnnotationKey) + sysctls, err := extensions.SysctlsFromPodSecurityPolicyAnnotation(sysctlAnnotation) + if err != nil { + allErrs = append(allErrs, field.Invalid(sysctlFldPath, sysctlAnnotation, err.Error())) + } else { + allErrs = append(allErrs, validatePodSecurityPolicySysctls(sysctlFldPath, sysctls)...) + } + return allErrs } @@ -674,6 +685,36 @@ func validatePodSecurityPolicyVolumes(fldPath *field.Path, volumes []extensions. return allErrs } +const sysctlPatternSegmentFmt string = "([a-z0-9][-_a-z0-9]*)?[a-z0-9*]" +const SysctlPatternFmt string = "(" + apivalidation.SysctlSegmentFmt + "\\.)*" + sysctlPatternSegmentFmt + +var sysctlPatternRegexp = regexp.MustCompile("^" + SysctlPatternFmt + "$") + +func IsValidSysctlPattern(name string) bool { + if len(name) > apivalidation.SysctlMaxLength { + return false + } + return sysctlPatternRegexp.MatchString(name) +} + +// validatePodSecurityPolicySysctls validates the sysctls fields of PodSecurityPolicy. +func validatePodSecurityPolicySysctls(fldPath *field.Path, sysctls []string) field.ErrorList { + allErrs := field.ErrorList{} + for i, s := range sysctls { + if !IsValidSysctlPattern(string(s)) { + allErrs = append( + allErrs, + field.Invalid(fldPath.Index(i), sysctls[i], fmt.Sprintf("must have at most %d characters and match regex %s", + apivalidation.SysctlMaxLength, + SysctlPatternFmt, + )), + ) + } + } + + return allErrs +} + // validateIDRanges ensures the range is valid. func validateIDRanges(fldPath *field.Path, rng extensions.IDRange) field.ErrorList { allErrs := field.ErrorList{} diff --git a/pkg/apis/extensions/validation/validation_test.go b/pkg/apis/extensions/validation/validation_test.go index 93e6f9b303a..1b4a366e67a 100644 --- a/pkg/apis/extensions/validation/validation_test.go +++ b/pkg/apis/extensions/validation/validation_test.go @@ -1512,7 +1512,8 @@ func TestValidatePodSecurityPolicy(t *testing.T) { validPSP := func() *extensions.PodSecurityPolicy { return &extensions.PodSecurityPolicy{ ObjectMeta: api.ObjectMeta{ - Name: "foo", + Name: "foo", + Annotations: map[string]string{}, }, Spec: extensions.PodSecurityPolicySpec{ SELinux: extensions.SELinuxStrategyOptions{ @@ -1596,6 +1597,9 @@ func TestValidatePodSecurityPolicy(t *testing.T) { apparmor.AllowedProfilesAnnotationKey: apparmor.ProfileRuntimeDefault + ",not-good", } + invalidSysctlPattern := validPSP() + invalidSysctlPattern.Annotations[extensions.SysctlsPodSecurityPolicyAnnotationKey] = "a.*.b" + errorCases := map[string]struct { psp *extensions.PodSecurityPolicy errorType field.ErrorType @@ -1686,6 +1690,11 @@ func TestValidatePodSecurityPolicy(t *testing.T) { errorType: field.ErrorTypeInvalid, errorDetail: "invalid AppArmor profile name: \"not-good\"", }, + "invalid sysctl pattern": { + psp: invalidSysctlPattern, + errorType: field.ErrorTypeInvalid, + errorDetail: fmt.Sprintf("must have at most 253 characters and match regex %s", SysctlPatternFmt), + }, } for k, v := range errorCases { @@ -1728,6 +1737,9 @@ func TestValidatePodSecurityPolicy(t *testing.T) { apparmor.AllowedProfilesAnnotationKey: apparmor.ProfileRuntimeDefault + "," + apparmor.ProfileNamePrefix + "foo", } + withSysctl := validPSP() + withSysctl.Annotations[extensions.SysctlsPodSecurityPolicyAnnotationKey] = "net.*" + successCases := map[string]struct { psp *extensions.PodSecurityPolicy }{ @@ -1749,6 +1761,9 @@ func TestValidatePodSecurityPolicy(t *testing.T) { "valid AppArmor annotations": { psp: validAppArmor, }, + "with network sysctls": { + psp: withSysctl, + }, } for k, v := range successCases { @@ -2031,6 +2046,58 @@ func TestValidateNetworkPolicyUpdate(t *testing.T) { } } +func TestIsValidSysctlPattern(t *testing.T) { + valid := []string{ + "a.b.c.d", + "a", + "a_b", + "a-b", + "abc", + "abc.def", + "*", + "a.*", + "*", + "abc*", + "a.abc*", + "a.b.*", + } + invalid := []string{ + "", + "รค", + "a_", + "_", + "_a", + "_a._b", + "__", + "-", + ".", + "a.", + ".a", + "a.b.", + "a*.b", + "a*b", + "*a", + "Abc", + func(n int) string { + x := make([]byte, n) + for i := range x { + x[i] = byte('a') + } + return string(x) + }(256), + } + for _, s := range valid { + if !IsValidSysctlPattern(s) { + t.Errorf("%q expected to be a valid sysctl pattern", s) + } + } + for _, s := range invalid { + if IsValidSysctlPattern(s) { + t.Errorf("%q expected to be an invalid sysctl pattern", s) + } + } +} + func newBool(val bool) *bool { p := new(bool) *p = val diff --git a/pkg/security/podsecuritypolicy/factory.go b/pkg/security/podsecuritypolicy/factory.go index f055c419876..8b187a45aba 100644 --- a/pkg/security/podsecuritypolicy/factory.go +++ b/pkg/security/podsecuritypolicy/factory.go @@ -25,6 +25,7 @@ import ( "k8s.io/kubernetes/pkg/security/podsecuritypolicy/capabilities" "k8s.io/kubernetes/pkg/security/podsecuritypolicy/group" "k8s.io/kubernetes/pkg/security/podsecuritypolicy/selinux" + "k8s.io/kubernetes/pkg/security/podsecuritypolicy/sysctl" "k8s.io/kubernetes/pkg/security/podsecuritypolicy/user" "k8s.io/kubernetes/pkg/util/errors" ) @@ -70,6 +71,19 @@ func (f *simpleStrategyFactory) CreateStrategies(psp *extensions.PodSecurityPoli errs = append(errs, err) } + var unsafeSysctls []string + if ann, found := psp.Annotations[extensions.SysctlsPodSecurityPolicyAnnotationKey]; found { + var err error + unsafeSysctls, err = extensions.SysctlsFromPodSecurityPolicyAnnotation(ann) + if err != nil { + errs = append(errs, err) + } + } + sysctlsStrat, err := createSysctlsStrategy(unsafeSysctls) + if err != nil { + errs = append(errs, err) + } + if len(errs) > 0 { return nil, errors.NewAggregate(errs) } @@ -81,6 +95,7 @@ func (f *simpleStrategyFactory) CreateStrategies(psp *extensions.PodSecurityPoli FSGroupStrategy: fsGroupStrat, SupplementalGroupStrategy: supGroupStrat, CapabilitiesStrategy: capStrat, + SysctlsStrategy: sysctlsStrat, } return strategies, nil @@ -145,3 +160,8 @@ func createSupplementalGroupStrategy(opts *extensions.SupplementalGroupsStrategy func createCapabilitiesStrategy(defaultAddCaps, requiredDropCaps, allowedCaps []api.Capability) (capabilities.Strategy, error) { return capabilities.NewDefaultCapabilities(defaultAddCaps, requiredDropCaps, allowedCaps) } + +// createSysctlsStrategy creates a new unsafe sysctls strategy. +func createSysctlsStrategy(sysctlsPatterns []string) (sysctl.SysctlsStrategy, error) { + return sysctl.NewMustMatchPatterns(sysctlsPatterns) +} diff --git a/pkg/security/podsecuritypolicy/provider.go b/pkg/security/podsecuritypolicy/provider.go index 82a6156a3ad..6856a3adb8a 100644 --- a/pkg/security/podsecuritypolicy/provider.go +++ b/pkg/security/podsecuritypolicy/provider.go @@ -210,6 +210,8 @@ func (s *simpleProvider) ValidatePodSecurityContext(pod *api.Pod, fldPath *field allErrs = append(allErrs, field.Invalid(fldPath.Child("hostIPC"), pod.Spec.SecurityContext.HostIPC, "Host IPC is not allowed to be used")) } + allErrs = append(allErrs, s.strategies.SysctlsStrategy.Validate(pod)...) + return allErrs } diff --git a/pkg/security/podsecuritypolicy/sysctl/mustmatchpatterns.go b/pkg/security/podsecuritypolicy/sysctl/mustmatchpatterns.go new file mode 100644 index 00000000000..66b11f71446 --- /dev/null +++ b/pkg/security/podsecuritypolicy/sysctl/mustmatchpatterns.go @@ -0,0 +1,92 @@ +/* +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 sysctl + +import ( + "fmt" + "strings" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/util/validation/field" +) + +// mustMatchPatterns implements the CapabilitiesStrategy interface +type mustMatchPatterns struct { + patterns []string +} + +var ( + _ SysctlsStrategy = &mustMatchPatterns{} + + defaultSysctlsPatterns = []string{"*"} +) + +// NewMustMatchPatterns creates a new mustMatchPattern strategy that will provide validation. +// Passing nil means the default pattern, passing an empty list means to disallow all sysctls. +func NewMustMatchPatterns(patterns []string) (SysctlsStrategy, error) { + if patterns == nil { + patterns = defaultSysctlsPatterns + } + return &mustMatchPatterns{ + patterns: patterns, + }, nil +} + +// Validate ensures that the specified values fall within the range of the strategy. +func (s *mustMatchPatterns) Validate(pod *api.Pod) field.ErrorList { + allErrs := field.ErrorList{} + allErrs = append(allErrs, s.validateAnnotation(pod, api.SysctlsPodAnnotationKey)...) + allErrs = append(allErrs, s.validateAnnotation(pod, api.UnsafeSysctlsPodAnnotationKey)...) + return allErrs +} + +func (s *mustMatchPatterns) validateAnnotation(pod *api.Pod, key string) field.ErrorList { + allErrs := field.ErrorList{} + + fieldPath := field.NewPath("pod", "metadata", "annotations").Key(key) + + sysctls, err := api.SysctlsFromPodAnnotation(pod.Annotations[key]) + if err != nil { + allErrs = append(allErrs, field.Invalid(fieldPath, pod.Annotations[key], err.Error())) + } + + if len(sysctls) > 0 { + if len(s.patterns) == 0 { + allErrs = append(allErrs, field.Invalid(fieldPath, pod.Annotations[key], "sysctls are not allowed")) + } else { + for i, sysctl := range sysctls { + allErrs = append(allErrs, s.ValidateSysctl(sysctl.Name, fieldPath.Index(i))...) + } + } + } + + return allErrs +} + +func (s *mustMatchPatterns) ValidateSysctl(sysctlName string, fldPath *field.Path) field.ErrorList { + for _, s := range s.patterns { + if s[len(s)-1] == '*' { + prefix := s[:len(s)-1] + if strings.HasPrefix(sysctlName, string(prefix)) { + return nil + } + } else if sysctlName == s { + return nil + } + } + return field.ErrorList{field.Forbidden(fldPath, fmt.Sprintf("sysctl %q is not allowed", sysctlName))} +} diff --git a/pkg/security/podsecuritypolicy/sysctl/mustmatchpatterns_test.go b/pkg/security/podsecuritypolicy/sysctl/mustmatchpatterns_test.go new file mode 100644 index 00000000000..23aefbcaabd --- /dev/null +++ b/pkg/security/podsecuritypolicy/sysctl/mustmatchpatterns_test.go @@ -0,0 +1,106 @@ +/* +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 sysctl + +import ( + "testing" + + "k8s.io/kubernetes/pkg/api" +) + +func TestValidate(t *testing.T) { + tests := map[string]struct { + patterns []string + allowed []string + disallowed []string + }{ + // no container requests + "nil": { + patterns: nil, + allowed: []string{"foo"}, + }, + "empty": { + patterns: []string{}, + disallowed: []string{"foo"}, + }, + "without wildcard": { + patterns: []string{"a", "a.b"}, + allowed: []string{"a", "a.b"}, + disallowed: []string{"b"}, + }, + "with catch-all wildcard": { + patterns: []string{"*"}, + allowed: []string{"a", "a.b"}, + }, + "with catch-all wildcard and non-wildcard": { + patterns: []string{"a.b.c", "*"}, + allowed: []string{"a", "a.b", "a.b.c", "b"}, + }, + "without catch-all wildcard": { + patterns: []string{"a.*", "b.*", "c.d.e", "d.e.f.*"}, + allowed: []string{"a.b", "b.c", "c.d.e", "d.e.f.g.h"}, + disallowed: []string{"a", "b", "c", "c.d", "d.e", "d.e.f"}, + }, + } + + for k, v := range tests { + strategy, err := NewMustMatchPatterns(v.patterns) + if err != nil { + t.Errorf("%s failed: %v", k, err) + continue + } + + pod := &api.Pod{} + errs := strategy.Validate(pod) + if len(errs) != 0 { + t.Errorf("%s: unexpected validaton errors for empty sysctls: %v", k, errs) + } + + sysctls := []api.Sysctl{} + for _, s := range v.allowed { + sysctls = append(sysctls, api.Sysctl{ + Name: s, + Value: "dummy", + }) + } + testAllowed := func(key string, category string) { + pod.Annotations = map[string]string{ + key: api.PodAnnotationsFromSysctls(sysctls), + } + errs = strategy.Validate(pod) + if len(errs) != 0 { + t.Errorf("%s: unexpected validaton errors for %s sysctls: %v", k, category, errs) + } + } + testDisallowed := func(key string, category string) { + for _, s := range v.disallowed { + pod.Annotations = map[string]string{ + key: api.PodAnnotationsFromSysctls([]api.Sysctl{{s, "dummy"}}), + } + errs = strategy.Validate(pod) + if len(errs) == 0 { + t.Errorf("%s: expected error for %s sysctl %q", k, category, s) + } + } + } + + testAllowed(api.SysctlsPodAnnotationKey, "safe") + testAllowed(api.UnsafeSysctlsPodAnnotationKey, "unsafe") + testDisallowed(api.SysctlsPodAnnotationKey, "safe") + testDisallowed(api.UnsafeSysctlsPodAnnotationKey, "unsafe") + } +} diff --git a/pkg/security/podsecuritypolicy/sysctl/types.go b/pkg/security/podsecuritypolicy/sysctl/types.go new file mode 100644 index 00000000000..6d41ed14c6b --- /dev/null +++ b/pkg/security/podsecuritypolicy/sysctl/types.go @@ -0,0 +1,28 @@ +/* +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 sysctl + +import ( + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/util/validation/field" +) + +// SysctlsStrategy defines the interface for all sysctl strategies. +type SysctlsStrategy interface { + // Validate ensures that the specified values fall within the range of the strategy. + Validate(pod *api.Pod) field.ErrorList +} diff --git a/pkg/security/podsecuritypolicy/types.go b/pkg/security/podsecuritypolicy/types.go index 7cf9104986b..d1d34fd16de 100644 --- a/pkg/security/podsecuritypolicy/types.go +++ b/pkg/security/podsecuritypolicy/types.go @@ -23,6 +23,7 @@ import ( "k8s.io/kubernetes/pkg/security/podsecuritypolicy/capabilities" "k8s.io/kubernetes/pkg/security/podsecuritypolicy/group" "k8s.io/kubernetes/pkg/security/podsecuritypolicy/selinux" + "k8s.io/kubernetes/pkg/security/podsecuritypolicy/sysctl" "k8s.io/kubernetes/pkg/security/podsecuritypolicy/user" "k8s.io/kubernetes/pkg/util/validation/field" ) @@ -63,4 +64,5 @@ type ProviderStrategies struct { FSGroupStrategy group.GroupStrategy SupplementalGroupStrategy group.GroupStrategy CapabilitiesStrategy capabilities.Strategy + SysctlsStrategy sysctl.SysctlsStrategy } diff --git a/plugin/pkg/admission/security/podsecuritypolicy/admission_test.go b/plugin/pkg/admission/security/podsecuritypolicy/admission_test.go index 53fc22ae281..a0631d6d2b2 100644 --- a/plugin/pkg/admission/security/podsecuritypolicy/admission_test.go +++ b/plugin/pkg/admission/security/podsecuritypolicy/admission_test.go @@ -26,7 +26,7 @@ import ( kadmission "k8s.io/kubernetes/pkg/admission" kapi "k8s.io/kubernetes/pkg/api" - extensions "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/apis/extensions" "k8s.io/kubernetes/pkg/auth/user" "k8s.io/kubernetes/pkg/client/cache" clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" @@ -34,7 +34,7 @@ import ( "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" + "k8s.io/kubernetes/pkg/util/diff" ) const defaultContainerName = "test-c" @@ -1028,6 +1028,151 @@ func TestAdmitReadOnlyRootFilesystem(t *testing.T) { } } +func TestAdmitSysctls(t *testing.T) { + podWithSysctls := func(safeSysctls []string, unsafeSysctls []string) *kapi.Pod { + pod := goodPod() + dummySysctls := func(names []string) []kapi.Sysctl { + sysctls := make([]kapi.Sysctl, len(names)) + for i, n := range names { + sysctls[i].Name = n + sysctls[i].Value = "dummy" + } + return sysctls + } + pod.Annotations[kapi.SysctlsPodAnnotationKey] = kapi.PodAnnotationsFromSysctls(dummySysctls(safeSysctls)) + pod.Annotations[kapi.UnsafeSysctlsPodAnnotationKey] = kapi.PodAnnotationsFromSysctls(dummySysctls(unsafeSysctls)) + return pod + } + + noSysctls := restrictivePSP() + noSysctls.Name = "no sysctls" + + emptySysctls := restrictivePSP() + emptySysctls.Name = "empty sysctls" + emptySysctls.Annotations[extensions.SysctlsPodSecurityPolicyAnnotationKey] = "" + + mixedSysctls := restrictivePSP() + mixedSysctls.Name = "wildcard sysctls" + mixedSysctls.Annotations[extensions.SysctlsPodSecurityPolicyAnnotationKey] = "a.*,b.*,c,d.e.f" + + aSysctl := restrictivePSP() + aSysctl.Name = "a sysctl" + aSysctl.Annotations[extensions.SysctlsPodSecurityPolicyAnnotationKey] = "a" + + bSysctl := restrictivePSP() + bSysctl.Name = "b sysctl" + bSysctl.Annotations[extensions.SysctlsPodSecurityPolicyAnnotationKey] = "b" + + cSysctl := restrictivePSP() + cSysctl.Name = "c sysctl" + cSysctl.Annotations[extensions.SysctlsPodSecurityPolicyAnnotationKey] = "c" + + catchallSysctls := restrictivePSP() + catchallSysctls.Name = "catchall sysctl" + catchallSysctls.Annotations[extensions.SysctlsPodSecurityPolicyAnnotationKey] = "*" + + tests := map[string]struct { + pod *kapi.Pod + psps []*extensions.PodSecurityPolicy + shouldPass bool + expectedPSP string + }{ + "pod without unsafe sysctls request allowed under noSysctls PSP": { + pod: goodPod(), + psps: []*extensions.PodSecurityPolicy{noSysctls}, + shouldPass: true, + expectedPSP: noSysctls.Name, + }, + "pod without any sysctls request allowed under emptySysctls PSP": { + pod: goodPod(), + psps: []*extensions.PodSecurityPolicy{emptySysctls}, + shouldPass: true, + expectedPSP: emptySysctls.Name, + }, + "pod with safe sysctls request allowed under noSysctls PSP": { + pod: podWithSysctls([]string{"a", "b"}, []string{}), + psps: []*extensions.PodSecurityPolicy{noSysctls}, + shouldPass: true, + expectedPSP: noSysctls.Name, + }, + "pod with unsafe sysctls request allowed under noSysctls PSP": { + pod: podWithSysctls([]string{}, []string{"a", "b"}), + psps: []*extensions.PodSecurityPolicy{noSysctls}, + shouldPass: true, + expectedPSP: noSysctls.Name, + }, + "pod with safe sysctls request disallowed under emptySysctls PSP": { + pod: podWithSysctls([]string{"a", "b"}, []string{}), + psps: []*extensions.PodSecurityPolicy{emptySysctls}, + shouldPass: false, + }, + "pod with unsafe sysctls request disallowed under emptySysctls PSP": { + pod: podWithSysctls([]string{}, []string{"a", "b"}), + psps: []*extensions.PodSecurityPolicy{emptySysctls}, + shouldPass: false, + }, + "pod with matching sysctls request allowed under mixedSysctls PSP": { + pod: podWithSysctls([]string{"a.b", "b.c"}, []string{"c", "d.e.f"}), + psps: []*extensions.PodSecurityPolicy{mixedSysctls}, + shouldPass: true, + expectedPSP: mixedSysctls.Name, + }, + "pod with not-matching unsafe sysctls request allowed under mixedSysctls PSP": { + pod: podWithSysctls([]string{"a.b", "b.c", "c", "d.e.f"}, []string{"e"}), + psps: []*extensions.PodSecurityPolicy{mixedSysctls}, + shouldPass: false, + }, + "pod with not-matching safe sysctls request allowed under mixedSysctls PSP": { + pod: podWithSysctls([]string{"a.b", "b.c", "c", "d.e.f", "e"}, []string{}), + psps: []*extensions.PodSecurityPolicy{mixedSysctls}, + shouldPass: false, + }, + "pod with sysctls request allowed under catchallSysctls PSP": { + pod: podWithSysctls([]string{"e"}, []string{"f"}), + psps: []*extensions.PodSecurityPolicy{catchallSysctls}, + shouldPass: true, + expectedPSP: catchallSysctls.Name, + }, + "pod with sysctls request allowed under catchallSysctls PSP, not under mixedSysctls or emptySysctls PSP": { + pod: podWithSysctls([]string{"e"}, []string{"f"}), + psps: []*extensions.PodSecurityPolicy{mixedSysctls, catchallSysctls, emptySysctls}, + shouldPass: true, + expectedPSP: catchallSysctls.Name, + }, + "pod with safe c sysctl request allowed under cSysctl PSP, not under aSysctl or bSysctl PSP": { + pod: podWithSysctls([]string{}, []string{"c"}), + psps: []*extensions.PodSecurityPolicy{aSysctl, bSysctl, cSysctl}, + shouldPass: true, + expectedPSP: cSysctl.Name, + }, + "pod with unsafe c sysctl request allowed under cSysctl PSP, not under aSysctl or bSysctl PSP": { + pod: podWithSysctls([]string{"c"}, []string{}), + psps: []*extensions.PodSecurityPolicy{aSysctl, bSysctl, cSysctl}, + shouldPass: true, + expectedPSP: cSysctl.Name, + }, + } + + for k, v := range tests { + origSafeSysctls, origUnsafeSysctls, err := kapi.SysctlsFromPodAnnotations(v.pod.Annotations) + if err != nil { + t.Fatalf("invalid sysctl annotation: %v", err) + } + + testPSPAdmit(k, v.psps, v.pod, v.shouldPass, v.expectedPSP, t) + + if v.shouldPass { + safeSysctls, unsafeSysctls, _ := kapi.SysctlsFromPodAnnotations(v.pod.Annotations) + if !reflect.DeepEqual(safeSysctls, origSafeSysctls) { + t.Errorf("%s: wrong safe sysctls: expected=%v, got=%v", k, origSafeSysctls, safeSysctls) + } + if !reflect.DeepEqual(unsafeSysctls, origUnsafeSysctls) { + t.Errorf("%s: wrong unsafe sysctls: expected=%v, got=%v", k, origSafeSysctls, safeSysctls) + } + } + } +} + func testPSPAdmit(testCaseName string, psps []*extensions.PodSecurityPolicy, pod *kapi.Pod, shouldPass bool, expectedPSP string, t *testing.T) { namespace := createNamespaceForTest() serviceAccount := createSAForTest() @@ -1044,17 +1189,17 @@ func testPSPAdmit(testCaseName string, psps []*extensions.PodSecurityPolicy, pod err := plugin.Admit(attrs) if shouldPass && err != nil { - t.Errorf("%s expected no errors but received %v", testCaseName, err) + t.Errorf("%s: expected no errors but received %v", testCaseName, err) } if shouldPass && err == nil { if pod.Annotations[psputil.ValidatedPSPAnnotation] != expectedPSP { - t.Errorf("%s expected to validate under %s but found %s", testCaseName, expectedPSP, pod.Annotations[psputil.ValidatedPSPAnnotation]) + t.Errorf("%s: expected to validate under %s but found %s", testCaseName, expectedPSP, pod.Annotations[psputil.ValidatedPSPAnnotation]) } } if !shouldPass && err == nil { - t.Errorf("%s expected errors but received none", testCaseName) + t.Errorf("%s: expected errors but received none", testCaseName) } } @@ -1238,7 +1383,8 @@ func TestCreateProvidersFromConstraints(t *testing.T) { func restrictivePSP() *extensions.PodSecurityPolicy { return &extensions.PodSecurityPolicy{ ObjectMeta: kapi.ObjectMeta{ - Name: "restrictive", + Name: "restrictive", + Annotations: map[string]string{}, }, Spec: extensions.PodSecurityPolicySpec{ RunAsUser: extensions.RunAsUserStrategyOptions{ @@ -1291,6 +1437,9 @@ func createSAForTest() *kapi.ServiceAccount { // psp when defaults are filled in. func goodPod() *kapi.Pod { return &kapi.Pod{ + ObjectMeta: kapi.ObjectMeta{ + Annotations: map[string]string{}, + }, Spec: kapi.PodSpec{ ServiceAccountName: "default", SecurityContext: &kapi.PodSecurityContext{},