Add MixinRestrictedPodSecurity e2e util

This commit is contained in:
Tim Allclair 2022-05-24 16:10:00 -07:00
parent f8c77fda0c
commit ccc69b1e9a
2 changed files with 172 additions and 1 deletions

View File

@ -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"}
}
}
}

View File

@ -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))
})
}