diff --git a/staging/src/k8s.io/pod-security-admission/policy/check_seccompProfile_restricted.go b/staging/src/k8s.io/pod-security-admission/policy/check_seccompProfile_restricted.go new file mode 100644 index 00000000000..031ffb11633 --- /dev/null +++ b/staging/src/k8s.io/pod-security-admission/policy/check_seccompProfile_restricted.go @@ -0,0 +1,138 @@ +/* +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" +) + +/* + +Seccomp profiles must be specified, and only runtime default and localhost profiles are allowed. + +v1.19+: +**Restricted Fields:** +spec.securityContext.seccompProfile.type +spec.containers[*].securityContext.seccompProfile.type +spec.initContainers[*].securityContext.seccompProfile.type + +**Allowed Values:** 'RuntimeDefault', 'Localhost' +Note: container-level fields may be undefined if pod-level field is specified. + +*/ + +func init() { + addCheck(CheckSeccompProfileRestricted) +} + +func CheckSeccompProfileRestricted() Check { + return Check{ + ID: "seccompProfile_restricted", + Level: api.LevelRestricted, + Versions: []VersionedCheck{ + { + MinimumVersion: api.MajorMinorVersion(1, 19), + CheckPod: seccompProfileRestricted_1_19, + }, + }, + } +} + +// seccompProfileRestricted_1_19 checks restricted policy on securityContext.seccompProfile field +func seccompProfileRestricted_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() + + podSeccompSet := false + + 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)) + } else { + podSeccompSet = true + } + } + + // containers that explicitly set seccompProfile.type to a bad value + var explicitlyBadContainers []string + // containers that didn't set seccompProfile and aren't caught by a pod-level seccompProfile + var implicitlyBadContainers []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)) + } + } else { + // container did not explicitly set seccompProfile + if !podSeccompSet { + // no valid pod-level seccompProfile, so this container implicitly has a bad value + implicitlyBadContainers = append(implicitlyBadContainers, c.Name) + } + } + }) + + 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()), + ), + } + } + + // pod didn't set seccompProfile and not all containers opted into seccompProfile + if len(implicitlyBadContainers) > 0 { + return CheckResult{ + Allowed: false, + ForbiddenReason: "seccompProfile", + ForbiddenDetail: fmt.Sprintf( + `pod or %s %s must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost"`, + pluralize("container", "containers", len(implicitlyBadContainers)), + joinQuote(implicitlyBadContainers), + ), + } + } + + return CheckResult{Allowed: true} +} diff --git a/staging/src/k8s.io/pod-security-admission/policy/check_seccompProfile_restricted_test.go b/staging/src/k8s.io/pod-security-admission/policy/check_seccompProfile_restricted_test.go new file mode 100644 index 00000000000..fec506bd527 --- /dev/null +++ b/staging/src/k8s.io/pod-security-admission/policy/check_seccompProfile_restricted_test.go @@ -0,0 +1,98 @@ +/* +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" +) + +func TestSeccompProfileRestricted(t *testing.T) { + tests := []struct { + name string + pod *corev1.Pod + expectReason string + expectDetail string + }{ + { + name: "no explicit seccomp", + pod: &corev1.Pod{Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "a"}, + }, + }}, + expectReason: `seccompProfile`, + expectDetail: `pod or container "a" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost"`, + }, + { + 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 nil, container fallthrough", + pod: &corev1.Pod{Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "a", SecurityContext: nil}, + {Name: "b", SecurityContext: &corev1.SecurityContext{}}, + {Name: "d", SecurityContext: &corev1.SecurityContext{SeccompProfile: &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeRuntimeDefault}}}, + {Name: "e", SecurityContext: &corev1.SecurityContext{SeccompProfile: &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeRuntimeDefault}}}, + }, + }}, + expectReason: `seccompProfile`, + expectDetail: `pod or containers "a", "b" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost"`, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result := seccompProfileRestricted_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_restricted.go b/staging/src/k8s.io/pod-security-admission/policy/check_seccomp_restricted.go deleted file mode 100644 index 75748c23f0d..00000000000 --- a/staging/src/k8s.io/pod-security-admission/policy/check_seccomp_restricted.go +++ /dev/null @@ -1,88 +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 ( - "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" -) - -func init() { - addCheck(CheckSeccompRestricted) -} - -func CheckSeccompRestricted() Check { - return Check{ - ID: "seccomp_restricted", - Level: api.LevelRestricted, - Versions: []VersionedCheck{ - { - MinimumVersion: api.MajorMinorVersion(1, 19), - CheckPod: seccomp_1_19_restricted, - }, - }, - } -} - -// seccomp_1_19_restricted checks restricted policy on securityContext.seccompProfile field -func seccomp_1_19_restricted(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) CheckResult { - forbidden := sets.NewString() - podSeccompField := field.NewPath("spec").Child("securityContext", "seccompProfile", "type") - podSeccompSet := false - - if podSpec.SecurityContext != nil { - if podSpec.SecurityContext.SeccompProfile != nil { - seccompType := podSpec.SecurityContext.SeccompProfile.Type - if !validSeccomp(podSpec.SecurityContext.SeccompProfile.Type) { - forbidden.Insert(fieldValue(podSeccompField, string(seccompType))) - } else { - podSeccompSet = true - } - } - } - - visitContainersWithPath(podSpec, field.NewPath("spec"), func(c *corev1.Container, path *field.Path) { - if c.SecurityContext != nil && c.SecurityContext.SeccompProfile != nil { - seccompType := c.SecurityContext.SeccompProfile.Type - if !validSeccomp(seccompType) { - containerSeccompField := path.Child("securityContext", "seccompProfile", "type") - forbidden.Insert(fieldValue(containerSeccompField, string(seccompType))) - } - return - } - - if !podSeccompSet { - containerSeccompField := path.Child("securityContext", "seccompProfile", "type") - forbidden.Insert(fieldValueRequired(containerSeccompField)) - } - }) - - 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_restricted.go b/staging/src/k8s.io/pod-security-admission/test/fixtures_seccompProfile_restricted.go similarity index 81% rename from staging/src/k8s.io/pod-security-admission/test/fixtures_seccomp_restricted.go rename to staging/src/k8s.io/pod-security-admission/test/fixtures_seccompProfile_restricted.go index a0d7306ea66..f26189486cb 100644 --- a/staging/src/k8s.io/pod-security-admission/test/fixtures_seccomp_restricted.go +++ b/staging/src/k8s.io/pod-security-admission/test/fixtures_seccompProfile_restricted.go @@ -30,7 +30,7 @@ The check implementation looks at the appropriate value based on version. func init() { fixtureData_restricted_1_19 := fixtureGenerator{ - expectErrorSubstring: "seccomp profile", + expectErrorSubstring: "seccompProfile", generatePass: func(p *corev1.Pod) []*corev1.Pod { p = ensureSecurityContext(p) return []*corev1.Pod{ @@ -43,11 +43,6 @@ func init() { tweak(p, func(p *corev1.Pod) { p.Spec.SecurityContext.SeccompProfile = nil p.Spec.Containers[0].SecurityContext.SeccompProfile = seccompProfileRuntimeDefault - p.Spec.InitContainers[0].SecurityContext.SeccompProfile = seccompProfileRuntimeDefault - }), - tweak(p, func(p *corev1.Pod) { - p.Spec.SecurityContext.SeccompProfile = nil - p.Spec.Containers[0].SecurityContext.SeccompProfile = seccompProfileLocalhost("testing") p.Spec.InitContainers[0].SecurityContext.SeccompProfile = seccompProfileLocalhost("testing") }), } @@ -55,36 +50,36 @@ func init() { generateFail: func(p *corev1.Pod) []*corev1.Pod { p = ensureSecurityContext(p) return []*corev1.Pod{ + // unset everywhere tweak(p, func(p *corev1.Pod) { p.Spec.SecurityContext.SeccompProfile = nil }), + // unconfined, pod-level tweak(p, func(p *corev1.Pod) { p.Spec.SecurityContext.SeccompProfile = seccompProfileUnconfined }), + // unset initContainer tweak(p, func(p *corev1.Pod) { p.Spec.SecurityContext.SeccompProfile = nil p.Spec.Containers[0].SecurityContext.SeccompProfile = seccompProfileRuntimeDefault }), + // unset container tweak(p, func(p *corev1.Pod) { p.Spec.SecurityContext.SeccompProfile = nil p.Spec.InitContainers[0].SecurityContext.SeccompProfile = seccompProfileRuntimeDefault }), + // unconfined, container-level tweak(p, func(p *corev1.Pod) { p.Spec.SecurityContext.SeccompProfile = nil p.Spec.Containers[0].SecurityContext.SeccompProfile = seccompProfileRuntimeDefault p.Spec.InitContainers[0].SecurityContext.SeccompProfile = seccompProfileUnconfined }), - tweak(p, func(p *corev1.Pod) { - p.Spec.SecurityContext.SeccompProfile = nil - p.Spec.Containers[0].SecurityContext.SeccompProfile = seccompProfileUnconfined - p.Spec.InitContainers[0].SecurityContext.SeccompProfile = seccompProfileRuntimeDefault - }), } }, } registerFixtureGenerator( - fixtureKey{level: api.LevelRestricted, version: api.MajorMinorVersion(1, 19), check: "seccomp_restricted"}, + fixtureKey{level: api.LevelRestricted, version: api.MajorMinorVersion(1, 19), check: "seccompProfile_restricted"}, fixtureData_restricted_1_19, ) }