Skip to content

Commit

Permalink
KEP-127: Update PSS based on feature gate
Browse files Browse the repository at this point in the history
Signed-off-by: Sascha Grunert <sgrunert@redhat.com>
  • Loading branch information
saschagrunert committed Oct 27, 2023
1 parent fd5c406 commit 77e0ade
Show file tree
Hide file tree
Showing 7 changed files with 112 additions and 11 deletions.
14 changes: 14 additions & 0 deletions pkg/features/kube_features.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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:

Expand Down
2 changes: 2 additions & 0 deletions plugin/pkg/admission/security/podsecurity/admission.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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 {
Expand Down
25 changes: 24 additions & 1 deletion staging/src/k8s.io/pod-security-admission/policy/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}

0 comments on commit 77e0ade

Please sign in to comment.