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
- Kubernetesapiserver v1.36.4, kubelet v1.36.3
- Runtimecontainerd 2.2.6
- CNICilium 1.18.1 - tunnel/VXLAN, with Hubble relay and UI
- Host OSUbuntu 26.04 LTS, kernel 7.0.0-29
- Built withkubeadm v1.36.3 - podSubnet 10.244.0.0/16, serviceSubnet 10.96.0.0/12
- TimeAbout 18 min
- Reviewed25 August 2026
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.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| CKA5001 | 192.168.0.41 | Ubuntu 26.04 LTS | Control Plane Node (tainted NoSchedule) | 2 Core | 4 GB | 50 GB |
| CKA5001-NODE01 | 192.168.0.42 | Ubuntu 26.04 LTS | Worker Node | 2 Core | 4 GB | 50 GB |
| CKA5001-NODE02 | 192.168.0.43 | Ubuntu 26.04 LTS | Worker Node | 2 Core | 4 GB | 50 GB |
| CKA5001-NODE03 | 192.168.0.44 | Ubuntu 26.04 LTS | Worker Node | 2 Core | 4 GB | 50 GB |
Before you start
- guide 6 - the rejection this page answers.
-
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 10001Expected result
pod/hardened created,condition met, scheduled tocka5001-node03withUSER 10001.Success conditionYou can write a Pod that a restricted namespace accepts.
-
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 rootExpected result
uid=10001 gid=0(root) groups=0(root).Success conditionYou know what runAsUser does and does not cover.
-
Capabilities gone, and seccomp actually loaded
Two proofs, read from
/procrather 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 result
CapPrm: 0000000000000000andCapEff: 0000000000000000.touchgivescommand terminated with exit code 1. AndSeccomp: 2withSeccomp_filters: 1.Success conditionYou can verify a control instead of trusting the manifest.
-
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 setExpected result
CapEff: 0000000000000000anduid 0.chownon that Pod terminates with exit code 1; the samechownon the default Pod printschown SUCCEEDED.Success conditionYou will not confuse dropping capabilities with not being root.
Troubleshooting
container has runAsNonRoot and image will run as root.Why:
runAsNonRoot: truewith norunAsUserand noUSERin the image.Fix:Set
runAsUserexplicitly, or build the image with aUSERdirective.A hardened Pod cannot write to its volume.
Why: The volume is owned by root and the process is not.
Fix:
fsGroupon the Pod securityContext, which chowns the volume to that group.An application breaks after dropping ALL capabilities.
Why: It genuinely needs one - often
CAP_NET_BIND_SERVICEfor a low port.Fix:Drop
ALL, thenaddthe single capability. Never start from the default set.readOnlyRootFilesystembreaks the container at startup.Why: Something writes to the image filesystem - a pid file, a cache, /tmp.
Fix:Mount an
emptyDirat each path it writes to.Unsure whether seccomp is actually applied.
Why: The manifest says one thing; the kernel is the authority.
Fix:
grep Seccomp /proc/self/statusinside the container. 2 means a filter is loaded.Assumed dropping capabilities means the container is not root.
Why: They are separate controls.
Fix:Check
id -u. Set bothrunAsNonRootandcapabilities.drop.