| /* |
| Copyright 2019 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 windows |
| |
| import ( |
| "context" |
| "fmt" |
| "strings" |
| "time" |
| |
| "github.com/onsi/ginkgo/v2" |
| "github.com/onsi/gomega" |
| v1 "k8s.io/api/core/v1" |
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| "k8s.io/apimachinery/pkg/fields" |
| "k8s.io/apimachinery/pkg/util/uuid" |
| clientset "k8s.io/client-go/kubernetes" |
| "k8s.io/kubernetes/pkg/kubelet/events" |
| "k8s.io/kubernetes/test/e2e/feature" |
| "k8s.io/kubernetes/test/e2e/framework" |
| e2epod "k8s.io/kubernetes/test/e2e/framework/pod" |
| e2eoutput "k8s.io/kubernetes/test/e2e/framework/pod/output" |
| testutils "k8s.io/kubernetes/test/utils" |
| imageutils "k8s.io/kubernetes/test/utils/image" |
| admissionapi "k8s.io/pod-security-admission/api" |
| ) |
| |
| const runAsUserNameContainerName = "run-as-username-container" |
| |
| var _ = sigDescribe(feature.Windows, "SecurityContext", skipUnlessWindows(func() { |
| f := framework.NewDefaultFramework("windows-run-as-username") |
| f.NamespacePodSecurityLevel = admissionapi.LevelPrivileged |
| |
| ginkgo.It("should be able create pods and run containers with a given username", func(ctx context.Context) { |
| ginkgo.By("Creating 2 pods: 1 with the default user, and one with a custom one.") |
| podDefault := runAsUserNamePod(nil) |
| e2eoutput.TestContainerOutput(ctx, f, "check default user", podDefault, 0, []string{"ContainerUser"}) |
| |
| podUserName := runAsUserNamePod(toPtr("ContainerAdministrator")) |
| e2eoutput.TestContainerOutput(ctx, f, "check set user", podUserName, 0, []string{"ContainerAdministrator"}) |
| }) |
| |
| ginkgo.It("should not be able to create pods with unknown usernames at Pod level", func(ctx context.Context) { |
| ginkgo.By("Creating a pod with an invalid username") |
| podInvalid := e2epod.NewPodClient(f).Create(ctx, runAsUserNamePod(toPtr("FooLish"))) |
| |
| failedSandboxEventSelector := fields.Set{ |
| "involvedObject.kind": "Pod", |
| "involvedObject.name": podInvalid.Name, |
| "involvedObject.namespace": podInvalid.Namespace, |
| "reason": events.FailedCreatePodSandBox, |
| }.AsSelector().String() |
| hcsschimError := "The user name or password is incorrect." |
| |
| // Hostprocess updated the cri to pass RunAsUserName to sandbox: https://github.com/kubernetes/kubernetes/pull/99576/commits/51a02fdb80cb7ba042a66362eb76facd2fd82401 |
| // Some runtimes might use that and set the username on the podsandbox. Containerd 1.6+ is known to do this. |
| // If there is an error when creating the pod sandbox then the pod stays in pending state by design |
| // See https://github.com/kubernetes/kubernetes/issues/104635 |
| // Not all runtimes use the sandbox information. This means the test needs to check if the pod |
| // sandbox failed or workload pod failed. |
| framework.Logf("Waiting for pod %s to enter the error state.", podInvalid.Name) |
| gomega.Eventually(ctx, func(ctx context.Context) bool { |
| failedSandbox, err := eventOccurred(ctx, f.ClientSet, podInvalid.Namespace, failedSandboxEventSelector, hcsschimError) |
| if err != nil { |
| framework.Logf("Error retrieving events for pod. Ignoring...") |
| } |
| if failedSandbox { |
| framework.Logf("Found Expected Event 'Failed to Create Pod Sandbox' with message containing: %s", hcsschimError) |
| return true |
| } |
| |
| framework.Logf("No Sandbox error found. Looking for failure in workload pods") |
| pod, err := e2epod.NewPodClient(f).Get(ctx, podInvalid.Name, metav1.GetOptions{}) |
| if err != nil { |
| framework.Logf("Error retrieving pod: %s", err) |
| return false |
| } |
| |
| podTerminatedReason := testutils.TerminatedContainers(pod)[runAsUserNameContainerName] |
| podFailedToStart := podTerminatedReason == "ContainerCannotRun" || podTerminatedReason == "StartError" |
| if pod.Status.Phase == v1.PodFailed && podFailedToStart { |
| framework.Logf("Found terminated workload Pod that could not start") |
| return true |
| } |
| |
| return false |
| }, framework.PodStartTimeout, 1*time.Second).Should(gomega.BeTrue()) |
| }) |
| |
| ginkgo.It("should not be able to create pods with unknown usernames at Container level", func(ctx context.Context) { |
| ginkgo.By("Creating a pod with an invalid username at container level and pod running as ContainerUser") |
| p := runAsUserNamePod(toPtr("FooLish")) |
| p.Spec.SecurityContext.WindowsOptions.RunAsUserName = toPtr("ContainerUser") |
| podInvalid := e2epod.NewPodClient(f).Create(ctx, p) |
| |
| framework.Logf("Waiting for pod %s to enter the error state.", podInvalid.Name) |
| framework.ExpectNoError(e2epod.WaitForPodTerminatedInNamespace(ctx, f.ClientSet, podInvalid.Name, "", f.Namespace.Name)) |
| |
| podInvalid, _ = e2epod.NewPodClient(f).Get(ctx, podInvalid.Name, metav1.GetOptions{}) |
| podTerminatedReason := testutils.TerminatedContainers(podInvalid)[runAsUserNameContainerName] |
| if podTerminatedReason != "ContainerCannotRun" && podTerminatedReason != "StartError" { |
| framework.Failf("The container terminated reason was supposed to be: 'ContainerCannotRun' or 'StartError', not: '%q'", podTerminatedReason) |
| } |
| }) |
| |
| ginkgo.It("should override SecurityContext username if set", func(ctx context.Context) { |
| ginkgo.By("Creating a pod with 2 containers with different username configurations.") |
| |
| pod := runAsUserNamePod(toPtr("ContainerAdministrator")) |
| pod.Spec.Containers[0].SecurityContext.WindowsOptions.RunAsUserName = toPtr("ContainerUser") |
| pod.Spec.Containers = append(pod.Spec.Containers, v1.Container{ |
| Name: "run-as-username-new-container", |
| Image: imageutils.GetE2EImage(imageutils.NonRoot), |
| Command: []string{"cmd", "/S", "/C", "echo %username%"}, |
| }) |
| |
| e2eoutput.TestContainerOutput(ctx, f, "check overridden username", pod, 0, []string{"ContainerUser"}) |
| e2eoutput.TestContainerOutput(ctx, f, "check pod SecurityContext username", pod, 1, []string{"ContainerAdministrator"}) |
| }) |
| |
| ginkgo.It("should ignore Linux Specific SecurityContext if set", func(ctx context.Context) { |
| ginkgo.By("Creating a pod with SELinux options") |
| // It is sufficient to show that the pod comes up here. Since we're stripping the SELinux and other linux |
| // security contexts in apiserver and not updating the pod object in the apiserver, we cannot validate the |
| // pod object to not have those security contexts. However the pod coming to running state is a sufficient |
| // enough condition for us to validate since prior to https://github.com/kubernetes/kubernetes/pull/93475 |
| // the pod would have failed to come up. |
| windowsPodWithSELinux := createTestPod(f, windowsBusyBoximage, windowsOS) |
| windowsPodWithSELinux.Spec.Containers[0].Args = []string{"test-webserver-with-selinux"} |
| windowsPodWithSELinux.Spec.SecurityContext = &v1.PodSecurityContext{} |
| containerUserName := "ContainerAdministrator" |
| windowsPodWithSELinux.Spec.SecurityContext.SELinuxOptions = &v1.SELinuxOptions{Level: "s0:c24,c9"} |
| windowsPodWithSELinux.Spec.Containers[0].SecurityContext = &v1.SecurityContext{ |
| SELinuxOptions: &v1.SELinuxOptions{Level: "s0:c24,c9"}, |
| WindowsOptions: &v1.WindowsSecurityContextOptions{RunAsUserName: &containerUserName}} |
| windowsPodWithSELinux.Spec.Tolerations = []v1.Toleration{{Key: "os", Value: "Windows"}} |
| windowsPodWithSELinux, err := f.ClientSet.CoreV1().Pods(f.Namespace.Name).Create(ctx, |
| windowsPodWithSELinux, metav1.CreateOptions{}) |
| framework.ExpectNoError(err) |
| framework.Logf("Created pod %v", windowsPodWithSELinux) |
| framework.ExpectNoError(e2epod.WaitForPodNameRunningInNamespace(ctx, f.ClientSet, windowsPodWithSELinux.Name, |
| f.Namespace.Name), "failed to wait for pod %s to be running", windowsPodWithSELinux.Name) |
| }) |
| |
| ginkgo.It("should not be able to create pods with containers running as ContainerAdministrator when runAsNonRoot is true", func(ctx context.Context) { |
| ginkgo.By("Creating a pod") |
| |
| p := runAsUserNamePod(toPtr("ContainerAdministrator")) |
| p.Spec.SecurityContext.RunAsNonRoot = &trueVar |
| |
| podInvalid, err := f.ClientSet.CoreV1().Pods(f.Namespace.Name).Create(ctx, p, metav1.CreateOptions{}) |
| framework.ExpectNoError(err, "Error creating pod") |
| |
| ginkgo.By("Waiting for pod to finish") |
| event, err := e2epod.NewPodClient(f).WaitForErrorEventOrSuccess(ctx, podInvalid) |
| framework.ExpectNoError(err) |
| gomega.Expect(event).ToNot(gomega.BeNil(), "event should not be empty") |
| framework.Logf("Got event: %v", event) |
| expectedEventError := "container's runAsUserName (ContainerAdministrator) which will be regarded as root identity and will break non-root policy" |
| gomega.Expect(event.Message).Should(gomega.ContainSubstring(expectedEventError), "Event error should indicate non-root policy caused container to not start") |
| }) |
| |
| ginkgo.It("should not be able to create pods with containers running as CONTAINERADMINISTRATOR when runAsNonRoot is true", func(ctx context.Context) { |
| ginkgo.By("Creating a pod") |
| |
| p := runAsUserNamePod(toPtr("CONTAINERADMINISTRATOR")) |
| p.Spec.SecurityContext.RunAsNonRoot = &trueVar |
| |
| podInvalid, err := f.ClientSet.CoreV1().Pods(f.Namespace.Name).Create(ctx, p, metav1.CreateOptions{}) |
| framework.ExpectNoError(err, "Error creating pod") |
| |
| ginkgo.By("Waiting for pod to finish") |
| event, err := e2epod.NewPodClient(f).WaitForErrorEventOrSuccess(ctx, podInvalid) |
| framework.ExpectNoError(err) |
| gomega.Expect(event).ToNot(gomega.BeNil(), "event should not be empty") |
| framework.Logf("Got event: %v", event) |
| expectedEventError := "container's runAsUserName (CONTAINERADMINISTRATOR) which will be regarded as root identity and will break non-root policy" |
| gomega.Expect(event.Message).Should(gomega.ContainSubstring(expectedEventError), "Event error should indicate non-root policy caused container to not start") |
| }) |
| })) |
| |
| func runAsUserNamePod(username *string) *v1.Pod { |
| podName := "run-as-username-" + string(uuid.NewUUID()) |
| return &v1.Pod{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: podName, |
| }, |
| Spec: v1.PodSpec{ |
| NodeSelector: map[string]string{"kubernetes.io/os": "windows"}, |
| Containers: []v1.Container{ |
| { |
| Name: runAsUserNameContainerName, |
| Image: imageutils.GetE2EImage(imageutils.NonRoot), |
| Command: []string{"cmd", "/S", "/C", "echo %username%"}, |
| SecurityContext: &v1.SecurityContext{ |
| WindowsOptions: &v1.WindowsSecurityContextOptions{ |
| RunAsUserName: username, |
| }, |
| }, |
| }, |
| }, |
| SecurityContext: &v1.PodSecurityContext{ |
| WindowsOptions: &v1.WindowsSecurityContextOptions{ |
| RunAsUserName: username, |
| }, |
| }, |
| RestartPolicy: v1.RestartPolicyNever, |
| }, |
| } |
| } |
| |
| func toPtr(s string) *string { |
| return &s |
| } |
| |
| func eventOccurred(ctx context.Context, c clientset.Interface, namespace, eventSelector, msg string) (bool, error) { |
| options := metav1.ListOptions{FieldSelector: eventSelector} |
| |
| events, err := c.CoreV1().Events(namespace).List(ctx, options) |
| if err != nil { |
| return false, fmt.Errorf("got error while getting events: %w", err) |
| } |
| for _, event := range events.Items { |
| if strings.Contains(event.Message, msg) { |
| return true, nil |
| } |
| } |
| return false, nil |
| } |