Merge pull request #118760 from saschagrunert/user-namespaces-pss
KEP-127: Update PSS based on feature gate
This commit is contained in:
		| @@ -886,6 +886,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() { | ||||
| @@ -1125,6 +1137,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: | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Kubernetes Prow Robot
					Kubernetes Prow Robot