From 77e0adeb31d251e3e426ad2bf7313cea235a79b3 Mon Sep 17 00:00:00 2001 From: Sascha Grunert Date: Tue, 20 Jun 2023 10:24:50 +0200 Subject: [PATCH] KEP-127: Update PSS based on feature gate Signed-off-by: Sascha Grunert --- pkg/features/kube_features.go | 14 ++++++++ .../security/podsecurity/admission.go | 2 ++ .../policy/check_runAsNonRoot.go | 5 +++ .../policy/check_runAsNonRoot_test.go | 36 ++++++++++++++++--- .../policy/check_runAsUser.go | 5 +++ .../policy/check_runAsUser_test.go | 36 ++++++++++++++++--- .../pod-security-admission/policy/helpers.go | 25 ++++++++++++- 7 files changed, 112 insertions(+), 11 deletions(-) diff --git a/pkg/features/kube_features.go b/pkg/features/kube_features.go index b1ff6aaf14b..9e6d68e9f6d 100644 --- a/pkg/features/kube_features.go +++ b/pkg/features/kube_features.go @@ -884,6 +884,18 @@ const ( // ImageMaximumGCAge enables the Kubelet configuration field of the same name, allowing an admin // to specify the age after which an image will be garbage collected. ImageMaximumGCAge featuregate.Feature = "ImageMaximumGCAge" + + // owner: @saschagrunert + // alpha: v1.28 + // + // Enables user namespace support for Pod Security Standards. Enabling this + // feature will modify all Pod Security Standard rules to allow setting: + // spec[.*].securityContext.[runAsNonRoot,runAsUser] + // This feature gate should only be enabled if all nodes in the cluster + // support the user namespace feature and have it enabled. The feature gate + // will not graduate or be enabled by default in future Kubernetes + // releases. + UserNamespacesPodSecurityStandards featuregate.Feature = "UserNamespacesPodSecurityStandards" ) func init() { @@ -1123,6 +1135,8 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS ImageMaximumGCAge: {Default: false, PreRelease: featuregate.Alpha}, + UserNamespacesPodSecurityStandards: {Default: false, PreRelease: featuregate.Alpha}, + // inherited features from generic apiserver, relisted here to get a conflict if it is changed // unintentionally on either side: diff --git a/plugin/pkg/admission/security/podsecurity/admission.go b/plugin/pkg/admission/security/podsecurity/admission.go index 2e99f11b6bb..e4b55cb4908 100644 --- a/plugin/pkg/admission/security/podsecurity/admission.go +++ b/plugin/pkg/admission/security/podsecurity/admission.go @@ -27,6 +27,7 @@ import ( _ "k8s.io/kubernetes/pkg/apis/apps/install" _ "k8s.io/kubernetes/pkg/apis/batch/install" _ "k8s.io/kubernetes/pkg/apis/core/install" + "k8s.io/kubernetes/pkg/features" admissionv1 "k8s.io/api/admission/v1" appsv1 "k8s.io/api/apps/v1" @@ -151,6 +152,7 @@ func (p *Plugin) updateDelegate() { func (c *Plugin) InspectFeatureGates(featureGates featuregate.FeatureGate) { c.inspectedFeatureGates = true + policy.RelaxPolicyForUserNamespacePods(featureGates.Enabled(features.UserNamespacesPodSecurityStandards)) } // ValidateInitialization ensures all required options are set diff --git a/staging/src/k8s.io/pod-security-admission/policy/check_runAsNonRoot.go b/staging/src/k8s.io/pod-security-admission/policy/check_runAsNonRoot.go index c8dfcfdde3d..87b83727b26 100644 --- a/staging/src/k8s.io/pod-security-admission/policy/check_runAsNonRoot.go +++ b/staging/src/k8s.io/pod-security-admission/policy/check_runAsNonRoot.go @@ -59,6 +59,11 @@ func CheckRunAsNonRoot() Check { } func runAsNonRoot_1_0(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) CheckResult { + // See KEP-127: https://github.com/kubernetes/enhancements/blob/308ba8d/keps/sig-node/127-user-namespaces/README.md?plain=1#L411-L447 + if relaxPolicyForUserNamespacePod(podSpec) { + return CheckResult{Allowed: true} + } + // things that explicitly set runAsNonRoot=false var badSetters []string diff --git a/staging/src/k8s.io/pod-security-admission/policy/check_runAsNonRoot_test.go b/staging/src/k8s.io/pod-security-admission/policy/check_runAsNonRoot_test.go index bda7f8db2f3..e8067496cdd 100644 --- a/staging/src/k8s.io/pod-security-admission/policy/check_runAsNonRoot_test.go +++ b/staging/src/k8s.io/pod-security-admission/policy/check_runAsNonRoot_test.go @@ -25,10 +25,12 @@ import ( func TestRunAsNonRoot(t *testing.T) { tests := []struct { - name string - pod *corev1.Pod - expectReason string - expectDetail string + name string + pod *corev1.Pod + expectReason string + expectDetail string + allowed bool + enableUserNamespacesPodSecurityStandards bool }{ { name: "no explicit runAsNonRoot", @@ -80,12 +82,36 @@ func TestRunAsNonRoot(t *testing.T) { expectReason: `runAsNonRoot != true`, expectDetail: `pod or containers "a", "b" must set securityContext.runAsNonRoot=true`, }, + { + name: "UserNamespacesPodSecurityStandards enabled without HostUsers", + pod: &corev1.Pod{Spec: corev1.PodSpec{ + HostUsers: utilpointer.Bool(false), + }}, + allowed: true, + enableUserNamespacesPodSecurityStandards: true, + }, + { + name: "UserNamespacesPodSecurityStandards enabled with HostUsers", + pod: &corev1.Pod{Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "a"}, + }, + HostUsers: utilpointer.Bool(true), + }}, + expectReason: `runAsNonRoot != true`, + expectDetail: `pod or container "a" must set securityContext.runAsNonRoot=true`, + allowed: false, + enableUserNamespacesPodSecurityStandards: true, + }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { + if tc.enableUserNamespacesPodSecurityStandards { + RelaxPolicyForUserNamespacePods(true) + } result := runAsNonRoot_1_0(&tc.pod.ObjectMeta, &tc.pod.Spec) - if result.Allowed { + if result.Allowed && !tc.allowed { t.Fatal("expected disallowed") } if e, a := tc.expectReason, result.ForbiddenReason; e != a { diff --git a/staging/src/k8s.io/pod-security-admission/policy/check_runAsUser.go b/staging/src/k8s.io/pod-security-admission/policy/check_runAsUser.go index de20f4d0ad4..eb9553ee04d 100644 --- a/staging/src/k8s.io/pod-security-admission/policy/check_runAsUser.go +++ b/staging/src/k8s.io/pod-security-admission/policy/check_runAsUser.go @@ -60,6 +60,11 @@ func CheckRunAsUser() Check { } func runAsUser_1_23(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) CheckResult { + // See KEP-127: https://github.com/kubernetes/enhancements/blob/308ba8d/keps/sig-node/127-user-namespaces/README.md?plain=1#L411-L447 + if relaxPolicyForUserNamespacePod(podSpec) { + return CheckResult{Allowed: true} + } + // things that explicitly set runAsUser=0 var badSetters []string diff --git a/staging/src/k8s.io/pod-security-admission/policy/check_runAsUser_test.go b/staging/src/k8s.io/pod-security-admission/policy/check_runAsUser_test.go index ac00535f974..490b07257cc 100644 --- a/staging/src/k8s.io/pod-security-admission/policy/check_runAsUser_test.go +++ b/staging/src/k8s.io/pod-security-admission/policy/check_runAsUser_test.go @@ -25,11 +25,12 @@ import ( func TestRunAsUser(t *testing.T) { tests := []struct { - name string - pod *corev1.Pod - expectAllow bool - expectReason string - expectDetail string + name string + pod *corev1.Pod + expectAllow bool + expectReason string + expectDetail string + enableUserNamespacesPodSecurityStandards bool }{ { name: "pod runAsUser=0", @@ -90,10 +91,35 @@ func TestRunAsUser(t *testing.T) { }}, expectAllow: true, }, + { + name: "UserNamespacesPodSecurityStandards enabled without HostUsers", + pod: &corev1.Pod{Spec: corev1.PodSpec{ + HostUsers: utilpointer.Bool(false), + }}, + expectAllow: true, + enableUserNamespacesPodSecurityStandards: true, + }, + { + name: "UserNamespacesPodSecurityStandards enabled with HostUsers", + pod: &corev1.Pod{Spec: corev1.PodSpec{ + SecurityContext: &corev1.PodSecurityContext{RunAsUser: utilpointer.Int64(0)}, + Containers: []corev1.Container{ + {Name: "a", SecurityContext: nil}, + }, + HostUsers: utilpointer.Bool(true), + }}, + expectAllow: false, + expectReason: `runAsUser=0`, + expectDetail: `pod must not set runAsUser=0`, + enableUserNamespacesPodSecurityStandards: true, + }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { + if tc.enableUserNamespacesPodSecurityStandards { + RelaxPolicyForUserNamespacePods(true) + } result := runAsUser_1_23(&tc.pod.ObjectMeta, &tc.pod.Spec) if tc.expectAllow { if !result.Allowed { diff --git a/staging/src/k8s.io/pod-security-admission/policy/helpers.go b/staging/src/k8s.io/pod-security-admission/policy/helpers.go index eb74f44e414..e35348929cf 100644 --- a/staging/src/k8s.io/pod-security-admission/policy/helpers.go +++ b/staging/src/k8s.io/pod-security-admission/policy/helpers.go @@ -16,7 +16,12 @@ limitations under the License. package policy -import "strings" +import ( + "strings" + "sync/atomic" + + corev1 "k8s.io/api/core/v1" +) func joinQuote(items []string) string { if len(items) == 0 { @@ -31,3 +36,21 @@ func pluralize(singular, plural string, count int) string { } return plural } + +var relaxPolicyForUserNamespacePods = &atomic.Bool{} + +// RelaxPolicyForUserNamespacePods allows opting into relaxing runAsUser / +// runAsNonRoot restricted policies for user namespace pods, before the +// usernamespace feature has reached GA and propagated to the oldest supported +// nodes. +// This should only be opted into in clusters where the administrator ensures +// all nodes in the cluster enable the user namespace feature. +func RelaxPolicyForUserNamespacePods(relax bool) { + relaxPolicyForUserNamespacePods.Store(relax) +} + +// relaxPolicyForUserNamespacePod returns true if a policy should be relaxed +// because of enabled user namespaces in the provided pod spec. +func relaxPolicyForUserNamespacePod(podSpec *corev1.PodSpec) bool { + return relaxPolicyForUserNamespacePods.Load() && podSpec != nil && podSpec.HostUsers != nil && !*podSpec.HostUsers +}