diff --git a/staging/src/k8s.io/pod-security-admission/policy/check_dropCapabilities.go b/staging/src/k8s.io/pod-security-admission/policy/check_dropCapabilities.go new file mode 100644 index 00000000000..602bbb286eb --- /dev/null +++ b/staging/src/k8s.io/pod-security-admission/policy/check_dropCapabilities.go @@ -0,0 +1,89 @@ +/* +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 ( + capabilityAll = "ALL" + capabilityNetBindService = "CAP_NET_BIND_SERVICE" +) + +func init() { + addCheck(CheckDropCapabilities) +} + +// CheckDropCapabilities returns a restricted level check +// that ensures all capabilities are dropped in 1.22+ +func CheckDropCapabilities() Check { + return Check{ + ID: "dropCapabilities", + Level: api.LevelRestricted, + Versions: []VersionedCheck{ + { + MinimumVersion: api.MajorMinorVersion(1, 22), + CheckPod: dropCapabilities_1_22, + }, + }, + } +} + +func dropCapabilities_1_22(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) CheckResult { + containers := sets.NewString() + visitContainersWithPath(podSpec, field.NewPath("spec"), func(container *corev1.Container, path *field.Path) { + if container.SecurityContext == nil || container.SecurityContext.Capabilities == nil { + containers.Insert(container.Name) + return + } + found := false + for _, c := range container.SecurityContext.Capabilities.Drop { + if c == capabilityAll { + found = true + break + } + } + if !found { + containers.Insert(container.Name) + return + } + for index, c := range container.SecurityContext.Capabilities.Add { + if c != capabilityNetBindService { + capabilityPath := path.Child("securityContext", "capabilities", "add").Index(index) + msg := fmt.Sprintf("%s=%s", capabilityPath.String(), string(c)) + containers.Insert(msg) + } + } + + }) + if len(containers) > 0 { + return CheckResult{ + Allowed: false, + ForbiddenReason: "containers must drop ALL capability", + ForbiddenDetail: strings.Join(containers.List(), ", "), + } + } + return CheckResult{Allowed: true} +} diff --git a/staging/src/k8s.io/pod-security-admission/test/fixtures_addCapabilities.go b/staging/src/k8s.io/pod-security-admission/test/fixtures_addCapabilities.go index 99d5b157201..5a00b015b1d 100644 --- a/staging/src/k8s.io/pod-security-admission/test/fixtures_addCapabilities.go +++ b/staging/src/k8s.io/pod-security-admission/test/fixtures_addCapabilities.go @@ -49,6 +49,13 @@ func init() { fixtureData_1_0 := fixtureGenerator{ expectErrorSubstring: "forbidden capabilities", generatePass: func(p *corev1.Pod) []*corev1.Pod { + // don't generate fixtures if minimal valid pod drops ALL + for _, capability := range p.Spec.Containers[0].SecurityContext.Capabilities.Drop { + if capability == corev1.Capability("ALL") { + return nil + } + } + p = ensureCapabilities(p) return []*corev1.Pod{ // all allowed capabilities diff --git a/staging/src/k8s.io/pod-security-admission/test/fixtures_dropCapabilities.go b/staging/src/k8s.io/pod-security-admission/test/fixtures_dropCapabilities.go new file mode 100644 index 00000000000..31893d873e8 --- /dev/null +++ b/staging/src/k8s.io/pod-security-admission/test/fixtures_dropCapabilities.go @@ -0,0 +1,102 @@ +/* +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 test + +import ( + corev1 "k8s.io/api/core/v1" + "k8s.io/pod-security-admission/api" +) + +/* +TODO: include field paths in reflect-based unit test +containerFields: []string{ + `securityContext.capabilities.drop`, +}, +*/ + +func init() { + fixtureData_1_22 := fixtureGenerator{ + expectErrorSubstring: "drop all", + generatePass: func(p *corev1.Pod) []*corev1.Pod { + return []*corev1.Pod{ + tweak(p, func(p *corev1.Pod) { + p.Spec.Containers[0].SecurityContext.Capabilities.Drop = []corev1.Capability{"ALL"} + p.Spec.InitContainers[0].SecurityContext.Capabilities.Drop = []corev1.Capability{"ALL"} + }), + tweak(p, func(p *corev1.Pod) { + p.Spec.Containers[0].SecurityContext.Capabilities.Drop = []corev1.Capability{"ALL"} + p.Spec.InitContainers[0].SecurityContext.Capabilities.Drop = []corev1.Capability{"ALL"} + p.Spec.Containers[0].SecurityContext.Capabilities.Add = []corev1.Capability{"CAP_NET_BIND_SERVICE"} + p.Spec.InitContainers[0].SecurityContext.Capabilities.Add = []corev1.Capability{"CAP_NET_BIND_SERVICE"} + }), + } + }, + generateFail: func(p *corev1.Pod) []*corev1.Pod { + p = ensureCapabilities(p) + return []*corev1.Pod{ + tweak(p, func(p *corev1.Pod) { + p.Spec.Containers[0].SecurityContext.Capabilities.Drop = []corev1.Capability{} + p.Spec.InitContainers[0].SecurityContext.Capabilities.Drop = []corev1.Capability{} + }), + tweak(p, func(p *corev1.Pod) { + p.Spec.Containers[0].SecurityContext.Capabilities.Drop = []corev1.Capability{ + "CAP_SYS_TIME", "CAP_SYS_MODULE", "CAP_SYS_RAWIO", "CAP_SYS_PACCT", "CAP_SYS_ADMIN", "CAP_SYS_NICE", + "CAP_SYS_RESOURCE", "CAP_SYS_TIME", "CAP_SYS_TTY_CONFIG", "CAP_MKNOD", "CAP_AUDIT_WRITE", + "CAP_AUDIT_CONTROL", "CAP_MAC_OVERRIDE", "CAP_MAC_ADMIN", "CAP_NET_ADMIN", "CAP_SYSLOG", + "CAP_CHOWN", "CAP_NET_RAW", "CAP_DAC_OVERRIDE", "CAP_FOWNER", "CAP_DAC_READ_SEARCH", + "CAP_FSETID", "CAP_KILL", "CAP_SETGID", "CAP_SETUID", "CAP_LINUX_IMMUTABLE", "CAP_NET_BIND_SERVICE", + "CAP_NET_BROADCAST", "CAP_IPC_LOCK", "CAP_IPC_OWNER", "CAP_SYS_CHROOT", "CAP_SYS_PTRACE", + "CAP_SYS_BOOT", "CAP_LEASE", "CAP_SETFCAP", "CAP_WAKE_ALARM", "CAP_BLOCK_SUSPEND", + } + p.Spec.InitContainers[0].SecurityContext.Capabilities.Drop = []corev1.Capability{ + "CAP_SYS_TIME", "CAP_SYS_MODULE", "CAP_SYS_RAWIO", "CAP_SYS_PACCT", "CAP_SYS_ADMIN", "CAP_SYS_NICE", + "CAP_SYS_RESOURCE", "CAP_SYS_TIME", "CAP_SYS_TTY_CONFIG", "CAP_MKNOD", "CAP_AUDIT_WRITE", + "CAP_AUDIT_CONTROL", "CAP_MAC_OVERRIDE", "CAP_MAC_ADMIN", "CAP_NET_ADMIN", "CAP_SYSLOG", + "CAP_CHOWN", "CAP_NET_RAW", "CAP_DAC_OVERRIDE", "CAP_FOWNER", "CAP_DAC_READ_SEARCH", + "CAP_FSETID", "CAP_KILL", "CAP_SETGID", "CAP_SETUID", "CAP_LINUX_IMMUTABLE", "CAP_NET_BIND_SERVICE", + "CAP_NET_BROADCAST", "CAP_IPC_LOCK", "CAP_IPC_OWNER", "CAP_SYS_CHROOT", "CAP_SYS_PTRACE", + "CAP_SYS_BOOT", "CAP_LEASE", "CAP_SETFCAP", "CAP_WAKE_ALARM", "CAP_BLOCK_SUSPEND", + } + }), + tweak(p, func(p *corev1.Pod) { + p.Spec.Containers[0].SecurityContext.Capabilities.Drop = []corev1.Capability{"ALL"} + p.Spec.InitContainers[0].SecurityContext.Capabilities.Drop = []corev1.Capability{"ALL"} + p.Spec.Containers[0].SecurityContext.Capabilities.Add = []corev1.Capability{ + "CAP_SYS_TIME", "CAP_SYS_MODULE", "CAP_SYS_RAWIO", "CAP_SYS_PACCT", "CAP_SYS_ADMIN", "CAP_SYS_NICE", + "CAP_SYS_RESOURCE", "CAP_SYS_TIME", "CAP_SYS_TTY_CONFIG", "CAP_MKNOD", "CAP_AUDIT_WRITE", + "CAP_AUDIT_CONTROL", "CAP_MAC_OVERRIDE", "CAP_MAC_ADMIN", "CAP_NET_ADMIN", "CAP_SYSLOG", + "CAP_CHOWN", "CAP_NET_RAW", "CAP_DAC_OVERRIDE", "CAP_FOWNER", "CAP_DAC_READ_SEARCH", + "CAP_FSETID", "CAP_KILL", "CAP_SETGID", "CAP_SETUID", "CAP_LINUX_IMMUTABLE", "CAP_NET_BROADCAST", + "CAP_IPC_LOCK", "CAP_IPC_OWNER", "CAP_SYS_CHROOT", "CAP_SYS_PTRACE", "CAP_SYS_BOOT", "CAP_LEASE", + "CAP_SETFCAP", "CAP_WAKE_ALARM", "CAP_BLOCK_SUSPEND", + } + p.Spec.InitContainers[0].SecurityContext.Capabilities.Add = []corev1.Capability{ + "CAP_SYS_TIME", "CAP_SYS_MODULE", "CAP_SYS_RAWIO", "CAP_SYS_PACCT", "CAP_SYS_ADMIN", "CAP_SYS_NICE", + "CAP_SYS_RESOURCE", "CAP_SYS_TIME", "CAP_SYS_TTY_CONFIG", "CAP_MKNOD", "CAP_AUDIT_WRITE", + "CAP_AUDIT_CONTROL", "CAP_MAC_OVERRIDE", "CAP_MAC_ADMIN", "CAP_NET_ADMIN", "CAP_SYSLOG", + "CAP_CHOWN", "CAP_NET_RAW", "CAP_DAC_OVERRIDE", "CAP_FOWNER", "CAP_DAC_READ_SEARCH", + "CAP_FSETID", "CAP_KILL", "CAP_SETGID", "CAP_SETUID", "CAP_LINUX_IMMUTABLE", "CAP_NET_BROADCAST", + "CAP_IPC_LOCK", "CAP_IPC_OWNER", "CAP_SYS_CHROOT", "CAP_SYS_PTRACE", "CAP_SYS_BOOT", "CAP_LEASE", + "CAP_SETFCAP", "CAP_WAKE_ALARM", "CAP_BLOCK_SUSPEND", + } + }), + } + }, + } + + registerFixtureGenerator( + fixtureKey{level: api.LevelRestricted, version: api.MajorMinorVersion(1, 22), check: "dropCapabilities"}, + fixtureData_1_22, + ) +} diff --git a/staging/src/k8s.io/pod-security-admission/test/testdata/restricted/v1.22/fail/dropcapabilities0.yaml b/staging/src/k8s.io/pod-security-admission/test/testdata/restricted/v1.22/fail/dropcapabilities0.yaml new file mode 100755 index 00000000000..e18a32b70b5 --- /dev/null +++ b/staging/src/k8s.io/pod-security-admission/test/testdata/restricted/v1.22/fail/dropcapabilities0.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Pod +metadata: + name: dropcapabilities0 +spec: + containers: + - image: k8s.gcr.io/pause + name: container1 + securityContext: + allowPrivilegeEscalation: false + capabilities: {} + initContainers: + - image: k8s.gcr.io/pause + name: initcontainer1 + securityContext: + allowPrivilegeEscalation: false + capabilities: {} + securityContext: + runAsNonRoot: true diff --git a/staging/src/k8s.io/pod-security-admission/test/testdata/restricted/v1.22/fail/dropcapabilities1.yaml b/staging/src/k8s.io/pod-security-admission/test/testdata/restricted/v1.22/fail/dropcapabilities1.yaml new file mode 100755 index 00000000000..a2259d96568 --- /dev/null +++ b/staging/src/k8s.io/pod-security-admission/test/testdata/restricted/v1.22/fail/dropcapabilities1.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Pod +metadata: + name: dropcapabilities1 +spec: + containers: + - image: k8s.gcr.io/pause + name: container1 + securityContext: + allowPrivilegeEscalation: false + capabilities: {} + initContainers: + - image: k8s.gcr.io/pause + name: initcontainer1 + securityContext: + allowPrivilegeEscalation: false + capabilities: {} + securityContext: + runAsNonRoot: true diff --git a/staging/src/k8s.io/pod-security-admission/test/testdata/restricted/v1.22/pass/dropcapabilities0.yaml b/staging/src/k8s.io/pod-security-admission/test/testdata/restricted/v1.22/pass/dropcapabilities0.yaml new file mode 100755 index 00000000000..1de1c842d4c --- /dev/null +++ b/staging/src/k8s.io/pod-security-admission/test/testdata/restricted/v1.22/pass/dropcapabilities0.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Pod +metadata: + name: dropcapabilities0 +spec: + containers: + - image: k8s.gcr.io/pause + name: container1 + securityContext: + allowPrivilegeEscalation: false + initContainers: + - image: k8s.gcr.io/pause + name: initcontainer1 + securityContext: + allowPrivilegeEscalation: false + securityContext: + runAsNonRoot: true