Skip to content

Commit

Permalink
Add MixinRestrictedPodSecurity e2e util
Browse files Browse the repository at this point in the history
  • Loading branch information
tallclair committed May 24, 2022
1 parent f8c77fd commit ccc69b1
Show file tree
Hide file tree
Showing 2 changed files with 172 additions and 1 deletion.
79 changes: 78 additions & 1 deletion test/e2e/framework/pod/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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},
}
}
Expand All @@ -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"}
}
}
}
94 changes: 94 additions & 0 deletions test/e2e/framework/pod/utils_test.go
Original file line number Diff line number Diff line change
@@ -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))
})

}

0 comments on commit ccc69b1

Please sign in to comment.