diff --git a/staging/src/k8s.io/pod-security-admission/policy/check_seccompProfile_baseline.go b/staging/src/k8s.io/pod-security-admission/policy/check_seccompProfile_baseline.go new file mode 100644 index 00000000000..ca1dd098ab4 --- /dev/null +++ b/staging/src/k8s.io/pod-security-admission/policy/check_seccompProfile_baseline.go @@ -0,0 +1,172 @@ +/* +Copyright 2021 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 policy + +import ( + "fmt" + "strings" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/pod-security-admission/api" +) + +/* + +If seccomp profiles are specified, only runtime default and localhost profiles are allowed. + +v1.0 - v1.18: +**Restricted Fields:** +metadata.annotations['seccomp.security.alpha.kubernetes.io/pod'] +metadata.annotations['container.seccomp.security.alpha.kubernetes.io/*'] + +**Allowed Values:** 'runtime/default', 'docker/default', 'localhost/*', undefined + +v1.19+: +**Restricted Fields:** +spec.securityContext.seccompProfile.type +spec.containers[*].securityContext.seccompProfile.type +spec.initContainers[*].securityContext.seccompProfile.type + +**Allowed Values:** 'RuntimeDefault', 'Localhost', undefined + +*/ +const ( + annotationKeyPod = "seccomp.security.alpha.kubernetes.io/pod" + annotationKeyContainerPrefix = "container.seccomp.security.alpha.kubernetes.io/" +) + +func init() { + addCheck(CheckSeccompBaseline) +} + +func CheckSeccompBaseline() Check { + return Check{ + ID: "seccompProfile_baseline", + Level: api.LevelBaseline, + Versions: []VersionedCheck{ + { + MinimumVersion: api.MajorMinorVersion(1, 0), + CheckPod: seccompProfileBaseline_1_0, + }, + { + MinimumVersion: api.MajorMinorVersion(1, 19), + CheckPod: seccompProfileBaseline_1_19, + }, + }, + } +} + +func validSeccomp(t corev1.SeccompProfileType) bool { + return t == corev1.SeccompProfileTypeLocalhost || + t == corev1.SeccompProfileTypeRuntimeDefault +} + +func validSeccompAnnotationValue(v string) bool { + return v == corev1.SeccompProfileRuntimeDefault || + v == corev1.DeprecatedSeccompProfileDockerDefault || + strings.HasPrefix(v, corev1.SeccompLocalhostProfileNamePrefix) +} + +// seccompProfileBaseline_1_0 checks baseline policy on seccomp alpha annotation +func seccompProfileBaseline_1_0(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) CheckResult { + forbidden := sets.NewString() + + if val, ok := podMetadata.Annotations[annotationKeyPod]; ok { + if !validSeccompAnnotationValue(val) { + forbidden.Insert(fmt.Sprintf("%s=%q", annotationKeyPod, val)) + } + } + + visitContainersWithPath(podSpec, field.NewPath("spec"), func(c *corev1.Container, path *field.Path) { + annotation := annotationKeyContainerPrefix + c.Name + if val, ok := podMetadata.Annotations[annotation]; ok { + if !validSeccompAnnotationValue(val) { + forbidden.Insert(fmt.Sprintf("%s=%q", annotation, val)) + } + } + }) + + if len(forbidden) > 0 { + return CheckResult{ + Allowed: false, + ForbiddenReason: "seccompProfile", + ForbiddenDetail: fmt.Sprintf( + "forbidden %s %s", + pluralize("annotation", "annotations", len(forbidden)), + strings.Join(forbidden.List(), ", "), + ), + } + } + + return CheckResult{Allowed: true} +} + +// seccompProfileBaseline_1_19 checks baseline policy on securityContext.seccompProfile field +func seccompProfileBaseline_1_19(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) CheckResult { + // things that explicitly set seccompProfile.type to a bad value + var badSetters []string + badValues := sets.NewString() + + if podSpec.SecurityContext != nil && podSpec.SecurityContext.SeccompProfile != nil { + if !validSeccomp(podSpec.SecurityContext.SeccompProfile.Type) { + badSetters = append(badSetters, "pod") + badValues.Insert(string(podSpec.SecurityContext.SeccompProfile.Type)) + } + } + + // containers that explicitly set seccompProfile.type to a bad value + var explicitlyBadContainers []string + + visitContainersWithPath(podSpec, field.NewPath("spec"), func(c *corev1.Container, path *field.Path) { + if c.SecurityContext != nil && c.SecurityContext.SeccompProfile != nil { + // container explicitly set seccompProfile + if !validSeccomp(c.SecurityContext.SeccompProfile.Type) { + // container explicitly set seccompProfile to a bad value + explicitlyBadContainers = append(explicitlyBadContainers, c.Name) + badValues.Insert(string(c.SecurityContext.SeccompProfile.Type)) + } + } + }) + + if len(explicitlyBadContainers) > 0 { + badSetters = append( + badSetters, + fmt.Sprintf( + "%s %s", + pluralize("container", "containers", len(explicitlyBadContainers)), + joinQuote(explicitlyBadContainers), + ), + ) + } + // pod or containers explicitly set bad seccompProfiles + if len(badSetters) > 0 { + return CheckResult{ + Allowed: false, + ForbiddenReason: "seccompProfile", + ForbiddenDetail: fmt.Sprintf( + "%s must not set securityContext.seccompProfile.type to %s", + strings.Join(badSetters, " and "), + joinQuote(badValues.List()), + ), + } + } + + return CheckResult{Allowed: true} +} diff --git a/staging/src/k8s.io/pod-security-admission/policy/check_seccompProfile_baseline_test.go b/staging/src/k8s.io/pod-security-admission/policy/check_seccompProfile_baseline_test.go new file mode 100644 index 00000000000..e5abb88daaf --- /dev/null +++ b/staging/src/k8s.io/pod-security-admission/policy/check_seccompProfile_baseline_test.go @@ -0,0 +1,162 @@ +/* +Copyright 2021 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 policy + +import ( + "testing" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestSeccompProfileBaseline_1_0(t *testing.T) { + tests := []struct { + name string + pod *corev1.Pod + expectReason string + expectDetail string + }{ + { + name: "pod seccomp invalid", + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "seccomp.security.alpha.kubernetes.io/pod": "unconfined", + }, + }, + }, + expectReason: `seccompProfile`, + expectDetail: `forbidden annotation seccomp.security.alpha.kubernetes.io/pod="unconfined"`, + }, + { + name: "containers seccomp invalid", + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "container.seccomp.security.alpha.kubernetes.io/a": "unconfined", + "container.seccomp.security.alpha.kubernetes.io/b": "unknown", + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{Name: "a"}, {Name: "b"}, {Name: "c"}}, + }, + }, + expectReason: `seccompProfile`, + expectDetail: `forbidden annotations container.seccomp.security.alpha.kubernetes.io/a="unconfined", container.seccomp.security.alpha.kubernetes.io/b="unknown"`, + }, + { + name: "pod and containers seccomp invalid", + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "seccomp.security.alpha.kubernetes.io/pod": "unconfined", + "container.seccomp.security.alpha.kubernetes.io/a": "unconfined", + "container.seccomp.security.alpha.kubernetes.io/b": "unknown", + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{Name: "a"}, {Name: "b"}, {Name: "c"}}, + }, + }, + expectReason: `seccompProfile`, + expectDetail: `forbidden annotations container.seccomp.security.alpha.kubernetes.io/a="unconfined", container.seccomp.security.alpha.kubernetes.io/b="unknown", seccomp.security.alpha.kubernetes.io/pod="unconfined"`, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result := seccompProfileBaseline_1_0(&tc.pod.ObjectMeta, &tc.pod.Spec) + if result.Allowed { + t.Fatal("expected disallowed") + } + if e, a := tc.expectReason, result.ForbiddenReason; e != a { + t.Errorf("expected\n%s\ngot\n%s", e, a) + } + if e, a := tc.expectDetail, result.ForbiddenDetail; e != a { + t.Errorf("expected\n%s\ngot\n%s", e, a) + } + }) + } +} + +func TestSeccompProfileBaseline_1_19(t *testing.T) { + tests := []struct { + name string + pod *corev1.Pod + expectReason string + expectDetail string + }{ + { + name: "pod seccomp invalid", + pod: &corev1.Pod{Spec: corev1.PodSpec{ + SecurityContext: &corev1.PodSecurityContext{SeccompProfile: &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeUnconfined}}, + Containers: []corev1.Container{ + {Name: "a", SecurityContext: nil}, + }, + }}, + expectReason: `seccompProfile`, + expectDetail: `pod must not set securityContext.seccompProfile.type to "Unconfined"`, + }, + { + name: "containers seccomp invalid", + pod: &corev1.Pod{Spec: corev1.PodSpec{ + SecurityContext: &corev1.PodSecurityContext{SeccompProfile: &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeRuntimeDefault}}, + Containers: []corev1.Container{ + {Name: "a", SecurityContext: nil}, + {Name: "b", SecurityContext: &corev1.SecurityContext{}}, + {Name: "c", SecurityContext: &corev1.SecurityContext{SeccompProfile: &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeUnconfined}}}, + {Name: "d", SecurityContext: &corev1.SecurityContext{SeccompProfile: &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeUnconfined}}}, + {Name: "e", SecurityContext: &corev1.SecurityContext{SeccompProfile: &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeRuntimeDefault}}}, + {Name: "f", SecurityContext: &corev1.SecurityContext{SeccompProfile: &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeRuntimeDefault}}}, + }, + }}, + expectReason: `seccompProfile`, + expectDetail: `containers "c", "d" must not set securityContext.seccompProfile.type to "Unconfined"`, + }, + { + name: "pod and containers seccomp invalid", + pod: &corev1.Pod{Spec: corev1.PodSpec{ + SecurityContext: &corev1.PodSecurityContext{SeccompProfile: &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeUnconfined}}, + Containers: []corev1.Container{ + {Name: "a", SecurityContext: nil}, + {Name: "b", SecurityContext: &corev1.SecurityContext{}}, + {Name: "c", SecurityContext: &corev1.SecurityContext{SeccompProfile: &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeUnconfined}}}, + {Name: "d", SecurityContext: &corev1.SecurityContext{SeccompProfile: &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeUnconfined}}}, + {Name: "e", SecurityContext: &corev1.SecurityContext{SeccompProfile: &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeRuntimeDefault}}}, + {Name: "f", SecurityContext: &corev1.SecurityContext{SeccompProfile: &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeRuntimeDefault}}}, + }, + }}, + expectReason: `seccompProfile`, + expectDetail: `pod and containers "c", "d" must not set securityContext.seccompProfile.type to "Unconfined"`, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result := seccompProfileBaseline_1_19(&tc.pod.ObjectMeta, &tc.pod.Spec) + if result.Allowed { + t.Fatal("expected disallowed") + } + if e, a := tc.expectReason, result.ForbiddenReason; e != a { + t.Errorf("expected\n%s\ngot\n%s", e, a) + } + if e, a := tc.expectDetail, result.ForbiddenDetail; e != a { + t.Errorf("expected\n%s\ngot\n%s", e, a) + } + }) + } +} diff --git a/staging/src/k8s.io/pod-security-admission/policy/check_seccomp_baseline.go b/staging/src/k8s.io/pod-security-admission/policy/check_seccomp_baseline.go deleted file mode 100644 index 829d3e47e58..00000000000 --- a/staging/src/k8s.io/pod-security-admission/policy/check_seccomp_baseline.go +++ /dev/null @@ -1,140 +0,0 @@ -/* -Copyright 2021 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 policy - -import ( - "fmt" - "strings" - - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/apimachinery/pkg/util/validation/field" - "k8s.io/pod-security-admission/api" -) - -const ( - annotationKeyPod = "seccomp.security.alpha.kubernetes.io/pod" - annotationKeyContainerPrefix = "container.seccomp.security.alpha.kubernetes.io/" - missingRequiredValue = "" -) - -func init() { - addCheck(CheckSeccompBaseline) -} - -func fieldValue(f *field.Path, val string) string { - return fmt.Sprintf("%s=%s", f.String(), val) -} - -func fieldValueRequired(f *field.Path) string { - return fmt.Sprintf("%s=%s", f.String(), missingRequiredValue) -} - -func CheckSeccompBaseline() Check { - return Check{ - ID: "seccomp_baseline", - Level: api.LevelBaseline, - Versions: []VersionedCheck{ - { - MinimumVersion: api.MajorMinorVersion(1, 0), - CheckPod: seccomp_1_0_baseline, - }, - { - MinimumVersion: api.MajorMinorVersion(1, 19), - CheckPod: seccomp_1_19_baseline, - }, - }, - } -} - -func validSeccomp(t corev1.SeccompProfileType) bool { - return t == corev1.SeccompProfileTypeLocalhost || - t == corev1.SeccompProfileTypeRuntimeDefault -} - -// seccomp_1_0_baseline checks baseline policy on seccomp alpha annotation -func seccomp_1_0_baseline(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) CheckResult { - forbidden := sets.NewString() - - if val, ok := podMetadata.Annotations[annotationKeyPod]; ok { - if val == corev1.SeccompProfileNameUnconfined { - podAnnotationField := field.NewPath("metadata").Child("annotations", annotationKeyPod) - forbidden.Insert(fieldValue(podAnnotationField, val)) - } - } - - visitContainersWithPath(podSpec, field.NewPath("spec"), func(c *corev1.Container, path *field.Path) { - annotation := annotationKeyContainerPrefix + c.Name - if val, ok := podMetadata.Annotations[annotation]; ok { - if val == corev1.SeccompProfileNameUnconfined { - containerAnnotationField := field.NewPath("metadata"). - Child("annotations", annotation) - forbidden.Insert(fieldValue(containerAnnotationField, val)) - } - } - }) - - if len(forbidden) > 0 { - return CheckResult{ - Allowed: false, - ForbiddenReason: "seccomp profile", - ForbiddenDetail: strings.Join(forbidden.List(), ", "), - } - } - - return CheckResult{Allowed: true} -} - -// seccomp_1_19_baseline checks baseline policy on securityContext.seccompProfile field -func seccomp_1_19_baseline(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) CheckResult { - forbidden := sets.NewString() - - if podSpec.SecurityContext != nil { - if podSpec.SecurityContext.SeccompProfile != nil { - seccompType := podSpec.SecurityContext.SeccompProfile.Type - if !validSeccomp(seccompType) { - podSeccompField := field.NewPath("spec").Child("securityContext", "seccompProfile", "type") - forbidden.Insert(fieldValue(podSeccompField, string(seccompType))) - } - } - } - - visitContainersWithPath(podSpec, field.NewPath("spec"), func(c *corev1.Container, path *field.Path) { - if c.SecurityContext != nil { - if c.SecurityContext.SeccompProfile != nil { - if c.SecurityContext.SeccompProfile.Type != "" { - seccompType := c.SecurityContext.SeccompProfile.Type - if !validSeccomp(seccompType) { - containerSeccompField := path.Child("securityContext", "seccompProfile", "type") - forbidden.Insert(fieldValue(containerSeccompField, string(seccompType))) - } - } - } - } - }) - - if len(forbidden) > 0 { - return CheckResult{ - Allowed: false, - ForbiddenReason: "seccomp profile", - ForbiddenDetail: strings.Join(forbidden.List(), ", "), - } - } - - return CheckResult{Allowed: true} -} diff --git a/staging/src/k8s.io/pod-security-admission/test/fixtures_seccomp_baseline.go b/staging/src/k8s.io/pod-security-admission/test/fixtures_seccompProfile_baseline.go similarity index 76% rename from staging/src/k8s.io/pod-security-admission/test/fixtures_seccomp_baseline.go rename to staging/src/k8s.io/pod-security-admission/test/fixtures_seccompProfile_baseline.go index 428a1f7f567..66d75c8947b 100644 --- a/staging/src/k8s.io/pod-security-admission/test/fixtures_seccomp_baseline.go +++ b/staging/src/k8s.io/pod-security-admission/test/fixtures_seccompProfile_baseline.go @@ -30,7 +30,7 @@ The check implementation looks at the appropriate value based on version. func init() { fixtureData_baseline_1_0 := fixtureGenerator{ - expectErrorSubstring: "seccomp profile", + expectErrorSubstring: "seccompProfile", generatePass: func(p *corev1.Pod) []*corev1.Pod { // don't generate fixtures if minimal valid pod already has seccomp config if val, ok := p.Annotations[annotationKeyPod]; ok && @@ -41,14 +41,11 @@ func init() { p = ensureAnnotation(p) return []*corev1.Pod{ tweak(p, func(p *corev1.Pod) { + // pod-level default p.Annotations[annotationKeyPod] = corev1.SeccompProfileRuntimeDefault - p.Annotations[annotationKeyContainer(p.Spec.Containers[0])] = corev1.SeccompProfileRuntimeDefault - p.Annotations[annotationKeyContainer(p.Spec.InitContainers[0])] = corev1.SeccompProfileRuntimeDefault - }), - tweak(p, func(p *corev1.Pod) { - p.Annotations[annotationKeyPod] = corev1.SeccompLocalhostProfileNamePrefix + "testing" + // container-level localhost p.Annotations[annotationKeyContainer(p.Spec.Containers[0])] = corev1.SeccompLocalhostProfileNamePrefix + "testing" - p.Annotations[annotationKeyContainer(p.Spec.InitContainers[0])] = corev1.SeccompLocalhostProfileNamePrefix + "testing" + // init-container unset }), } }, @@ -69,7 +66,7 @@ func init() { } fixtureData_baseline_1_19 := fixtureGenerator{ - expectErrorSubstring: "seccomp profile", + expectErrorSubstring: "seccompProfile", generatePass: func(p *corev1.Pod) []*corev1.Pod { // don't generate fixtures if minimal valid pod already has seccomp config if p.Spec.SecurityContext != nil && @@ -81,14 +78,11 @@ func init() { p = ensureSecurityContext(p) return []*corev1.Pod{ tweak(p, func(p *corev1.Pod) { + // pod-level default p.Spec.SecurityContext.SeccompProfile = seccompProfileRuntimeDefault + // container-level localhost p.Spec.Containers[0].SecurityContext.SeccompProfile = seccompProfileRuntimeDefault - p.Spec.InitContainers[0].SecurityContext.SeccompProfile = seccompProfileRuntimeDefault - }), - tweak(p, func(p *corev1.Pod) { - p.Spec.SecurityContext.SeccompProfile = seccompProfileLocalhost("testing") - p.Spec.Containers[0].SecurityContext.SeccompProfile = seccompProfileLocalhost("testing") - p.Spec.InitContainers[0].SecurityContext.SeccompProfile = seccompProfileLocalhost("testing") + // init-container unset }), } }, @@ -109,12 +103,12 @@ func init() { } registerFixtureGenerator( - fixtureKey{level: api.LevelBaseline, version: api.MajorMinorVersion(1, 0), check: "seccomp_baseline"}, + fixtureKey{level: api.LevelBaseline, version: api.MajorMinorVersion(1, 0), check: "seccompProfile_baseline"}, fixtureData_baseline_1_0, ) registerFixtureGenerator( - fixtureKey{level: api.LevelBaseline, version: api.MajorMinorVersion(1, 19), check: "seccomp_baseline"}, + fixtureKey{level: api.LevelBaseline, version: api.MajorMinorVersion(1, 19), check: "seccompProfile_baseline"}, fixtureData_baseline_1_19, ) }