diff --git a/test/e2e/framework/pod/utils.go b/test/e2e/framework/pod/utils.go index d6a88bf2f4f..66487a92e9d 100644 --- a/test/e2e/framework/pod/utils.go +++ b/test/e2e/framework/pod/utils.go @@ -18,9 +18,14 @@ package pod import ( "flag" + "fmt" + + "github.com/onsi/gomega" v1 "k8s.io/api/core/v1" imageutils "k8s.io/kubernetes/test/utils/image" + psaapi "k8s.io/pod-security-admission/api" + psapolicy "k8s.io/pod-security-admission/policy" "k8s.io/utils/pointer" ) @@ -115,10 +120,16 @@ func GetLinuxLabel() *v1.SELinuxOptions { Level: "s0:c0,c1"} } -// GetRestrictedPodSecurityContext returns a minimal restricted pod security context. +// DefaultNonRootUser is the default user ID used for running restricted (non-root) containers. +const DefaultNonRootUser = 1000 + +// GetRestrictedPodSecurityContext returns a restricted pod security context. +// This includes setting RunAsUser for convenience, to pass the RunAsNonRoot check. +// Tests that require a specific user ID should override this. func GetRestrictedPodSecurityContext() *v1.PodSecurityContext { return &v1.PodSecurityContext{ RunAsNonRoot: pointer.BoolPtr(true), + RunAsUser: pointer.Int64(DefaultNonRootUser), SeccompProfile: &v1.SeccompProfile{Type: v1.SeccompProfileTypeRuntimeDefault}, } } @@ -130,3 +141,69 @@ func GetRestrictedContainerSecurityContext() *v1.SecurityContext { Capabilities: &v1.Capabilities{Drop: []v1.Capability{"ALL"}}, } } + +var psaEvaluator, _ = psapolicy.NewEvaluator(psapolicy.DefaultChecks()) + +// MustMixinRestrictedPodSecurity makes the given pod compliant with the restricted pod security level. +// If doing so would overwrite existing non-conformant configuration, a test failure is triggered. +func MustMixinRestrictedPodSecurity(pod *v1.Pod) *v1.Pod { + err := MixinRestrictedPodSecurity(pod) + gomega.ExpectWithOffset(1, err).NotTo(gomega.HaveOccurred()) + return pod +} + +// MixinRestrictedPodSecurity makes the given pod compliant with the restricted pod security level. +// If doing so would overwrite existing non-conformant configuration, an error is returned. +// Note that this sets a default RunAsUser. See GetRestrictedPodSecurityContext. +// TODO(#105919): Handle PodOS for windows pods. +func MixinRestrictedPodSecurity(pod *v1.Pod) error { + if pod.Spec.SecurityContext == nil { + pod.Spec.SecurityContext = GetRestrictedPodSecurityContext() + } else { + if pod.Spec.SecurityContext.RunAsNonRoot == nil { + pod.Spec.SecurityContext.RunAsNonRoot = pointer.BoolPtr(true) + } + if pod.Spec.SecurityContext.RunAsUser == nil { + pod.Spec.SecurityContext.RunAsUser = pointer.Int64Ptr(DefaultNonRootUser) + } + if pod.Spec.SecurityContext.SeccompProfile == nil { + pod.Spec.SecurityContext.SeccompProfile = &v1.SeccompProfile{Type: v1.SeccompProfileTypeRuntimeDefault} + } + } + for i := range pod.Spec.Containers { + mixinRestrictedContainerSecurityContext(&pod.Spec.Containers[i]) + } + for i := range pod.Spec.InitContainers { + mixinRestrictedContainerSecurityContext(&pod.Spec.InitContainers[i]) + } + + // Validate the resulting pod against the restricted profile. + restricted := psaapi.LevelVersion{ + Level: psaapi.LevelRestricted, + Version: psaapi.LatestVersion(), + } + if agg := psapolicy.AggregateCheckResults(psaEvaluator.EvaluatePod(restricted, &pod.ObjectMeta, &pod.Spec)); !agg.Allowed { + return fmt.Errorf("failed to make pod %s restricted: %s", pod.Name, agg.ForbiddenDetail()) + } + + return nil +} + +// mixinRestrictedContainerSecurityContext adds the required container security context options to +// be compliant with the restricted pod security level. Non-conformance checking is handled by the +// caller. +func mixinRestrictedContainerSecurityContext(container *v1.Container) { + if container.SecurityContext == nil { + container.SecurityContext = GetRestrictedContainerSecurityContext() + } else { + if container.SecurityContext.AllowPrivilegeEscalation == nil { + container.SecurityContext.AllowPrivilegeEscalation = pointer.Bool(false) + } + if container.SecurityContext.Capabilities == nil { + container.SecurityContext.Capabilities = &v1.Capabilities{} + } + if len(container.SecurityContext.Capabilities.Drop) == 0 { + container.SecurityContext.Capabilities.Drop = []v1.Capability{"ALL"} + } + } +} diff --git a/test/e2e/framework/pod/utils_test.go b/test/e2e/framework/pod/utils_test.go new file mode 100644 index 00000000000..cc93ee0bae5 --- /dev/null +++ b/test/e2e/framework/pod/utils_test.go @@ -0,0 +1,94 @@ +/* +Copyright 2022 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 pod + +import ( + "testing" + + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/pointer" +) + +func TestMixinRestrictedPodSecurity(t *testing.T) { + restrictablePods := []v1.Pod{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{{ + Name: "pause", + Image: "pause", + }}, + }, + }, { + ObjectMeta: metav1.ObjectMeta{ + Name: "already_restricted", + }, + Spec: v1.PodSpec{ + SecurityContext: GetRestrictedPodSecurityContext(), + Containers: []v1.Container{{ + Name: "pause", + Image: "pause", + SecurityContext: GetRestrictedContainerSecurityContext(), + }}, + }, + }, { + ObjectMeta: metav1.ObjectMeta{ + Name: "empty_securityContext", + }, + Spec: v1.PodSpec{ + SecurityContext: &v1.PodSecurityContext{}, + Containers: []v1.Container{{ + Name: "pause", + Image: "pause", + SecurityContext: &v1.SecurityContext{}, + }}, + }, + }} + + for _, pod := range restrictablePods { + t.Run(pod.Name, func(t *testing.T) { + p := pod // closure + assert.NoError(t, MixinRestrictedPodSecurity(&p)) + assert.Equal(t, GetRestrictedPodSecurityContext(), p.Spec.SecurityContext, + "Mixed in PodSecurityContext should equal the from-scratch PodSecurityContext") + assert.Equal(t, GetRestrictedContainerSecurityContext(), p.Spec.Containers[0].SecurityContext, + "Mixed in SecurityContext should equal the from-scratch SecurityContext") + }) + } + + privilegedPod := v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "privileged", + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{{ + Name: "pause", + Image: "pause", + SecurityContext: &v1.SecurityContext{ + Privileged: pointer.Bool(true), + }, + }}, + }, + } + t.Run("privileged", func(t *testing.T) { + assert.Error(t, MixinRestrictedPodSecurity(&privilegedPod)) + }) + +}