CertGrid CertGrid
Hands-on Lab·Kubernetes and Cloud Native Security Associate

Hardening a Pod with securityContext

The previous page had a Pod refused for four separate violations. This one fixes all four, gets it admitted into a `restricted` namespace, and then proves each control is actually in force rather than merely declared.

Cloud Native Security Guide 7 of 42 Intermediate

Written against the versions above. `securityContext` fields are stable API. `seccompProfile: RuntimeDefault` uses the container runtime's default filter - containerd 2.2.6 here - and is what `restricted` requires.

The cka5001 cluster: one control plane and 3 schedulable workers, on Cilium.
Server NameIP AddressOSRolesCPURAMHDD
CKA5001192.168.0.41Ubuntu 26.04 LTSControl Plane Node (tainted NoSchedule)2 Core4 GB50 GB
CKA5001-NODE01192.168.0.42Ubuntu 26.04 LTSWorker Node2 Core4 GB50 GB
CKA5001-NODE02192.168.0.43Ubuntu 26.04 LTSWorker Node2 Core4 GB50 GB
CKA5001-NODE03192.168.0.44Ubuntu 26.04 LTSWorker Node2 Core4 GB50 GB

Before you start

  1. The spec that satisfies restricted

    Five settings. Submitted to the namespace that refused the last Pod.

    bash Example session
    printf 'apiVersion: v1\nkind: Pod\nmetadata:\n  name: hardened\nspec:\n  containers:\n  - name: app\n    image: docker.io/library/busybox:1.37\n    command: ["sh","-c","sleep 600"]\n    securityContext:\n      runAsNonRoot: true\n      runAsUser: 10001\n      allowPrivilegeEscalation: false\n      readOnlyRootFilesystem: true\n      capabilities:\n        drop: ["ALL"]\n      seccompProfile:\n        type: RuntimeDefault\n' | kubectl -n kcsa-strict apply -f - 2>&1 | tail -2pod/hardened createdkubectl -n kcsa-strict wait --for=condition=Ready pod/hardened --timeout=120s; kubectl -n kcsa-strict get pod hardened -o custom-columns='NAME:.metadata.name,NODE:.spec.nodeName,USER:.spec.containers[0].securityContext.runAsUser'pod/hardened condition metNAME       NODE             USERhardened   cka5001-node03   10001

    Expected resultpod/hardened created, condition met, scheduled to cka5001-node03 with USER 10001.

    Success conditionYou can write a Pod that a restricted namespace accepts.

  2. Not root, but look at the group

    Two facts in one line, and the second is easy to miss.

    bash Example session
    kubectl -n kcsa-strict exec hardened -- id; echo "--- not root"uid=10001 gid=0(root) groups=0(root)--- not root

    Expected resultuid=10001 gid=0(root) groups=0(root).

    Success conditionYou know what runAsUser does and does not cover.

  3. Capabilities gone, and seccomp actually loaded

    Two proofs, read from /proc rather than from the spec.

    bash Example session
    kubectl -n kcsa-strict exec hardened -- sh -c 'grep -E "^CapEff|^CapPrm" /proc/self/status'; echo "--- every capability dropped. A default Pod carries 00000000a80425fb"CapPrm:	0000000000000000CapEff:	0000000000000000--- every capability dropped. A default Pod carries 00000000a80425fbkubectl -n kcsa-strict exec hardened -- sh -c 'touch /newfile' 2>&1 | tail -1; echo "--- readOnlyRootFilesystem refuses the write"command terminated with exit code 1--- readOnlyRootFilesystem refuses the writekubectl -n kcsa-strict exec hardened -- sh -c 'grep -E "^Seccomp" /proc/self/status'; echo "--- Seccomp 2 = a filter is loaded (0 disabled, 1 strict, 2 filtered)"Seccomp:	2Seccomp_filters:	1--- Seccomp 2 = a filter is loaded (0 disabled, 1 strict, 2 filtered)

    Expected resultCapPrm: 0000000000000000 and CapEff: 0000000000000000. touch gives command terminated with exit code 1. And Seccomp: 2 with Seccomp_filters: 1.

    Success conditionYou can verify a control instead of trusting the manifest.

  4. The control that does less than people think

    Drop every capability and check the uid.

    bash Example session
    kubectl -n kcsa-open run capdrop --image=docker.io/library/busybox:1.37 --restart=Never --overrides='{"spec":{"containers":[{"name":"capdrop","image":"docker.io/library/busybox:1.37","command":["sh","-c","sleep 600"],"securityContext":{"capabilities":{"drop":["ALL"]}}}]}}' >/dev/null 2>&1; kubectl -n kcsa-open wait --for=condition=Ready pod/capdrop --timeout=120s >/dev/null; kubectl -n kcsa-open exec capdrop -- sh -c 'grep ^CapEff /proc/self/status; echo "uid $(id -u)"'CapEff:	0000000000000000uid 0kubectl -n kcsa-open exec capdrop -- sh -c 'chown 1000 /tmp' 2>&1 | tail -1; echo "--- root WITHOUT CAP_CHOWN cannot chown"command terminated with exit code 1--- root WITHOUT CAP_CHOWN cannot chownkubectl -n kcsa-open exec plain -- sh -c 'chown 1000 /tmp && echo "chown SUCCEEDED"'; echo "--- and the default Pod can, because CAP_CHOWN is in the default set"chown SUCCEEDED--- and the default Pod can, because CAP_CHOWN is in the default set

    Expected resultCapEff: 0000000000000000 and uid 0. chown on that Pod terminates with exit code 1; the same chown on the default Pod prints chown SUCCEEDED.

    Success conditionYou will not confuse dropping capabilities with not being root.

Troubleshooting

Official sources