Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restricted Pod E2E tests #109946

Merged
merged 2 commits into from
May 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
96 changes: 49 additions & 47 deletions test/e2e/common/node/pods.go
Expand Up @@ -184,7 +184,7 @@ func expectNoErrorWithRetries(fn func() error, maxRetries int, explain ...interf

var _ = SIGDescribe("Pods", func() {
f := framework.NewDefaultFramework("pods")
f.NamespacePodSecurityEnforceLevel = admissionapi.LevelBaseline
liggitt marked this conversation as resolved.
Show resolved Hide resolved
f.NamespacePodSecurityEnforceLevel = admissionapi.LevelRestricted
var podClient *framework.PodClient
var dc dynamic.Interface

Expand All @@ -200,7 +200,7 @@ var _ = SIGDescribe("Pods", func() {
*/
framework.ConformanceIt("should get a host IP [NodeConformance]", func() {
name := "pod-hostip-" + string(uuid.NewUUID())
testHostIP(podClient, &v1.Pod{
testHostIP(podClient, e2epod.MustMixinRestrictedPodSecurity(&v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Expand All @@ -212,7 +212,7 @@ var _ = SIGDescribe("Pods", func() {
},
},
},
})
}))
})

/*
Expand All @@ -224,7 +224,7 @@ var _ = SIGDescribe("Pods", func() {
ginkgo.By("creating the pod")
name := "pod-submit-remove-" + string(uuid.NewUUID())
value := strconv.Itoa(time.Now().Nanosecond())
pod := &v1.Pod{
tallclair marked this conversation as resolved.
Show resolved Hide resolved
pod := e2epod.MustMixinRestrictedPodSecurity(&v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Labels: map[string]string{
Expand All @@ -235,12 +235,12 @@ var _ = SIGDescribe("Pods", func() {
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: "nginx",
Image: imageutils.GetE2EImage(imageutils.Nginx),
Name: "pause",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does changing from nginx to pause change the lifecycle characteristics of the pod (w.r.t. handling shutdown signals)? same question applies in multiple places

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. Both the pause container and nginx handle SIGINT and SIGTERM as a fast shutdown, which I think is what containerd uses. nginx additionally handles SIGQUIT, but I don't see any references to SIGQUIT in k/k, containerd, or cri-o.

Maybe @SergeyKanzhelev or @endocrimes have additional context.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test doesn't cover any corner cases of shutdown so this seems ok to me 👍

Image: imageutils.GetPauseImageName(),
},
},
},
}
})

ginkgo.By("setting up watch")
selector := labels.SelectorFromSet(labels.Set(map[string]string{"time": value}))
Expand Down Expand Up @@ -342,7 +342,7 @@ var _ = SIGDescribe("Pods", func() {
ginkgo.By("creating the pod")
name := "pod-update-" + string(uuid.NewUUID())
value := strconv.Itoa(time.Now().Nanosecond())
pod := &v1.Pod{
pod := e2epod.MustMixinRestrictedPodSecurity(&v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Labels: map[string]string{
Expand All @@ -353,12 +353,12 @@ var _ = SIGDescribe("Pods", func() {
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: "nginx",
Image: imageutils.GetE2EImage(imageutils.Nginx),
Name: "pause",
Image: imageutils.GetPauseImageName(),
},
},
},
}
})

ginkgo.By("submitting the pod to kubernetes")
pod = podClient.CreateSync(pod)
Expand Down Expand Up @@ -396,7 +396,7 @@ var _ = SIGDescribe("Pods", func() {
ginkgo.By("creating the pod")
name := "pod-update-activedeadlineseconds-" + string(uuid.NewUUID())
value := strconv.Itoa(time.Now().Nanosecond())
pod := &v1.Pod{
pod := e2epod.MustMixinRestrictedPodSecurity(&v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Labels: map[string]string{
Expand All @@ -407,18 +407,18 @@ var _ = SIGDescribe("Pods", func() {
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: "nginx",
Image: imageutils.GetE2EImage(imageutils.Nginx),
Name: "pause",
Image: imageutils.GetPauseImageName(),
},
},
},
}
})

ginkgo.By("submitting the pod to kubernetes")
podClient.CreateSync(pod)

ginkgo.By("verifying the pod is in kubernetes")
selector := labels.SelectorFromSet(labels.Set(map[string]string{"time": value}))
selector := labels.SelectorFromSet(labels.Set{"time": value})
options := metav1.ListOptions{LabelSelector: selector.String()}
pods, err := podClient.List(context.TODO(), options)
framework.ExpectNoError(err, "failed to query for pods")
Expand All @@ -442,7 +442,7 @@ var _ = SIGDescribe("Pods", func() {
// Make a pod that will be a service.
// This pod serves its hostname via HTTP.
serverName := "server-envvars-" + string(uuid.NewUUID())
serverPod := &v1.Pod{
serverPod := e2epod.MustMixinRestrictedPodSecurity(&v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: serverName,
Labels: map[string]string{"name": serverName},
Expand All @@ -456,7 +456,7 @@ var _ = SIGDescribe("Pods", func() {
},
},
},
}
})
podClient.CreateSync(serverPod)

// This service exposes port 8080 of the test pod as a service on port 8765
Expand Down Expand Up @@ -490,7 +490,7 @@ var _ = SIGDescribe("Pods", func() {
// Make a client pod that verifies that it has the service environment variables.
podName := "client-envvars-" + string(uuid.NewUUID())
const containerName = "env3cont"
pod := &v1.Pod{
pod := e2epod.MustMixinRestrictedPodSecurity(&v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: podName,
Labels: map[string]string{"name": podName},
Expand All @@ -505,7 +505,7 @@ var _ = SIGDescribe("Pods", func() {
},
RestartPolicy: v1.RestartPolicyNever,
},
}
})

// It's possible for the Pod to be created before the Kubelet is updated with the new
// service. In that case, we just retry.
Expand Down Expand Up @@ -536,7 +536,7 @@ var _ = SIGDescribe("Pods", func() {

ginkgo.By("creating the pod")
name := "pod-exec-websocket-" + string(uuid.NewUUID())
pod := &v1.Pod{
pod := e2epod.MustMixinRestrictedPodSecurity(&v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Expand All @@ -549,7 +549,7 @@ var _ = SIGDescribe("Pods", func() {
},
},
},
}
})

ginkgo.By("submitting the pod to kubernetes")
pod = podClient.CreateSync(pod)
Expand Down Expand Up @@ -618,7 +618,7 @@ var _ = SIGDescribe("Pods", func() {

ginkgo.By("creating the pod")
name := "pod-logs-websocket-" + string(uuid.NewUUID())
pod := &v1.Pod{
pod := e2epod.MustMixinRestrictedPodSecurity(&v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Expand All @@ -631,7 +631,7 @@ var _ = SIGDescribe("Pods", func() {
},
},
},
}
})

ginkgo.By("submitting the pod to kubernetes")
podClient.CreateSync(pod)
Expand Down Expand Up @@ -673,7 +673,7 @@ var _ = SIGDescribe("Pods", func() {
ginkgo.It("should have their auto-restart back-off timer reset on image update [Slow][NodeConformance]", func() {
podName := "pod-back-off-image"
containerName := "back-off"
pod := &v1.Pod{
pod := e2epod.MustMixinRestrictedPodSecurity(&v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: podName,
Labels: map[string]string{"test": "back-off-image"},
Expand All @@ -687,7 +687,7 @@ var _ = SIGDescribe("Pods", func() {
},
},
},
}
})

delay1, delay2 := startPodAndGetBackOffs(podClient, pod, buildBackOffDuration)

Expand All @@ -714,7 +714,7 @@ var _ = SIGDescribe("Pods", func() {
ginkgo.It("should cap back-off at MaxContainerBackOff [Slow][NodeConformance]", func() {
podName := "back-off-cap"
containerName := "back-off-cap"
pod := &v1.Pod{
pod := e2epod.MustMixinRestrictedPodSecurity(&v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: podName,
Labels: map[string]string{"test": "liveness"},
Expand All @@ -728,7 +728,7 @@ var _ = SIGDescribe("Pods", func() {
},
},
},
}
})

podClient.CreateSync(pod)
time.Sleep(2 * kubelet.MaxContainerBackOff) // it takes slightly more than 2*x to get to a back-off of x
Expand Down Expand Up @@ -770,7 +770,7 @@ var _ = SIGDescribe("Pods", func() {
readinessGate1 := "k8s.io/test-condition1"
readinessGate2 := "k8s.io/test-condition2"
patchStatusFmt := `{"status":{"conditions":[{"type":%q, "status":%q}]}}`
pod := &v1.Pod{
pod := e2epod.MustMixinRestrictedPodSecurity(&v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: podName,
Labels: map[string]string{"test": "pod-readiness-gate"},
Expand All @@ -788,7 +788,7 @@ var _ = SIGDescribe("Pods", func() {
{ConditionType: v1.PodConditionType(readinessGate2)},
},
},
}
})

validatePodReadiness := func(expectReady bool) {
err := wait.Poll(time.Second, time.Minute, func() (bool, error) {
Expand Down Expand Up @@ -843,20 +843,22 @@ var _ = SIGDescribe("Pods", func() {
ginkgo.By("Create set of pods")
// create a set of pods in test namespace
for _, podTestName := range podTestNames {
_, err := f.ClientSet.CoreV1().Pods(f.Namespace.Name).Create(context.TODO(), &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: podTestName,
Labels: map[string]string{
"type": "Testing"},
},
Spec: v1.PodSpec{
TerminationGracePeriodSeconds: &one,
Containers: []v1.Container{{
Image: imageutils.GetE2EImage(imageutils.Agnhost),
Name: "token-test",
}},
RestartPolicy: v1.RestartPolicyNever,
}}, metav1.CreateOptions{})
_, err := f.ClientSet.CoreV1().Pods(f.Namespace.Name).Create(context.TODO(),
e2epod.MustMixinRestrictedPodSecurity(&v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: podTestName,
Labels: map[string]string{
"type": "Testing",
},
},
Spec: v1.PodSpec{
TerminationGracePeriodSeconds: &one,
Containers: []v1.Container{{
Image: imageutils.GetE2EImage(imageutils.Agnhost),
Name: "token-test",
}},
RestartPolicy: v1.RestartPolicyNever,
}}), metav1.CreateOptions{})
framework.ExpectNoError(err, "failed to create pod")
framework.Logf("created %v", podTestName)
}
Expand Down Expand Up @@ -903,7 +905,7 @@ var _ = SIGDescribe("Pods", func() {
podsList, err := f.ClientSet.CoreV1().Pods("").List(context.TODO(), metav1.ListOptions{LabelSelector: testPodLabelsFlat})
framework.ExpectNoError(err, "failed to list Pods")

testPod := v1.Pod{
testPod := e2epod.MustMixinRestrictedPodSecurity(&v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: testPodName,
Labels: testPodLabels,
Expand All @@ -917,9 +919,9 @@ var _ = SIGDescribe("Pods", func() {
},
},
},
}
})
ginkgo.By("creating a Pod with a static label")
_, err = f.ClientSet.CoreV1().Pods(testNamespaceName).Create(context.TODO(), &testPod, metav1.CreateOptions{})
_, err = f.ClientSet.CoreV1().Pods(testNamespaceName).Create(context.TODO(), testPod, metav1.CreateOptions{})
framework.ExpectNoError(err, "failed to create Pod %v in namespace %v", testPod.ObjectMeta.Name, testNamespaceName)

ginkgo.By("watching for Pod to be ready")
Expand Down
79 changes: 78 additions & 1 deletion test/e2e/framework/pod/utils.go
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),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do windows nodes do if runAsUser is non-nil? do they ignore it or fail?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is ignored by kubelet:

if effectiveSc.RunAsUser != nil {
klog.InfoS("Windows container does not support SecurityContext.RunAsUser, please use SecurityContext.WindowsOptions",
"pod", klog.KObj(pod), "containerName", container.Name)
}

With PodOS feature and OS field is set (in beta in 1.24) the adminsion controller would reject it:

// If the OS field is set to windows, following fields must be unset:
// - spec.hostPID
// - spec.hostIPC
// - spec.securityContext.seLinuxOptions
// - spec.securityContext.seccompProfile
// - spec.securityContext.fsGroup
// - spec.securityContext.fsGroupChangePolicy
// - spec.securityContext.sysctls
// - spec.shareProcessNamespace
// - spec.securityContext.runAsUser
// - spec.securityContext.runAsGroup
// - spec.securityContext.supplementalGroups
// - spec.containers[*].securityContext.seLinuxOptions
// - spec.containers[*].securityContext.seccompProfile
// - spec.containers[*].securityContext.capabilities
// - spec.containers[*].securityContext.readOnlyRootFilesystem
// - spec.containers[*].securityContext.privileged
// - spec.containers[*].securityContext.allowPrivilegeEscalation
// - spec.containers[*].securityContext.procMount
// - spec.containers[*].securityContext.runAsUser
// - spec.containers[*].securityContext.runAsGroup
// +optional

Copy link
Member

@liggitt liggitt May 25, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, there's a TODO to avoid setting RunAsUser in MustMixinRestrictedPodSecurity for explicitly windows pods as part of the PodOS work, I just wanted to make sure this wasn't going to fail in a windows kubelet for pods that didn't explicitly indicate their OS

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, this did break the tests because of #102849

https://prow.k8s.io/view/gs/kubernetes-jenkins/logs/ci-kubernetes-e2e-capz-master-containerd-windows/1529769915945324544

May 26 10:47:49.396: INFO: At 2022-05-26 10:44:57 +0000 UTC - event for pod-update-d046cc17-42c7-493c-8c0c-fea0e9ac3164: {kubelet capz-conf-qwm24} FailedMount: (combined from similar events): MountVolume.SetUp failed for volume "kube-api-access-9l7hz" : chown c:\var\lib\kubelet\pods\0a6b5b55-fc8f-42b4-b285-19aedc311882\volumes\kubernetes.io~projected\kube-api-access-9l7hz\..2022_05_26_10_46_59.624505313\token: not supported by windows

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this isn't setting RunAsUserName, but does the windows kubelet have similar problems with RunAsUser?

if so, that seems like a good reason to drop RunAsUser here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes the title is slightly misleading but it states in the issue: In addition if a Windows Pod is created with a Projected Volume and RunAsUser set, the Pod will be stuck at ContainerCreating:

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok. @tallclair, that's a good reason not to set RunAsUser here for pods that could run on Windows. @jsturtevant, do you mind opening a PR to drop the RunAsUser field?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

linking here for anyone following the thread #110235

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())
tallclair marked this conversation as resolved.
Show resolved Hide resolved
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 {
tallclair marked this conversation as resolved.
Show resolved Hide resolved
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 {
liggitt marked this conversation as resolved.
Show resolved Hide resolved
container.SecurityContext.Capabilities.Drop = []v1.Capability{"ALL"}
}
}
}