koti.dev
← The Runbook
Mastering Kubernetes the Right Way · DAY 30 / 35

I Ran Kubernetes Pods as Root for 2 Years. Then the Auditor Called.

runAsNonRoot, runAsUser, fsGroup, allowPrivilegeEscalation. The four lines that turn a SOC2 finding from red to green.

KV
Koti Vellanki18 Apr 20263 min read
kubernetessecurity
I Ran Kubernetes Pods as Root for 2 Years. Then the Auditor Called.

11PM on a Tuesday, SOC2 auditor sends a spreadsheet. Row 47, highlighted red: "Containers running as UID 0. Non-compliant. Blocker for certification." I open the deployment yaml we had been running in prod for two years and there is no securityContext block at all. Not a single one, not in the pod, not in the container. The base image had USER root in the Dockerfile and Kubernetes had just trusted it. 37 deployments to patch by morning. The worst part was that I had written most of them myself, back when I thought "it works" was the same as "it is safe."

The scenario

DAY 30 · SECURITY · SECURITY CONTEXT

The policy said: no root. The image disagreed.

The pod's securityContext sets runAsNonRoot: true. The container image was built with USER root. The kubelet inspects the image config at runtime and refuses to start the container before any process is exec'd. The pod never runs.

FIGURE30 / 35
runAsNonRoot vs USER root — kubelet refuses container startA pod sets securityContext.runAsNonRoot: true. The image was built with USER root. The kubelet checks the image config at runtime, finds a contradiction, and refuses to start the container. The pod status shows: container has runAsNonRoot and image will run as root.KUBERNETES CLUSTERproduction · us-east-1 · v1.30POD · default nssecurityContext:runAsNonRoot: trueno runAsUser setuid resolved from image→ uid 0 = contradiction1start reqimage inspectKUBELET admissionruntime checkpod requests:nonRootimage USER:root (uid 0)→ contradictionkubelet/kuberuntime/security_context.gorefusing to start2start refusedCONTAINERDimage inspectimage config:USER rootWORKDIR /apprefusing to starterror: container hasrunAsNonRoot andimage will run as root3
1

The pod declares runAsNonRoot but no runAsUser

Setting runAsNonRoot: true without runAsUser tells the kubelet: resolve the uid from the image, then reject if it is 0. The policy intent is correct — the execution is incomplete.

2

The kubelet catches the contradiction before exec

The kubelet asks containerd to inspect the image config, reads USER root, resolves uid 0, and compares it against runAsNonRoot: true. The check happens in security_context.go — before any process is exec'd. The pod never starts.

3

Fix it in the image or in the pod spec

Two valid fixes: add USER 1000 (any non-zero uid) to the Dockerfile, or set securityContext.runAsUser: 1000 in the pod spec to override the image default. The Dockerfile fix is preferable — it ensures the image is safe regardless of the deployment context.

Kubernetes
Kubelet admission
Refused start
◆ koti.dev / runbook
A pod sets runAsNonRoot: true but the image USER directive resolves to uid 0 — kubelet refuses to start.
A Kubernetes pod has securityContext.runAsNonRoot set to true. The kubelet asks containerd to inspect the image config and discovers the USER field is root, which resolves to uid 0. This contradicts the runAsNonRoot policy. Kubelet refuses to start the container and records the error: container has runAsNonRoot and image will run as root.
pod.spec.securityContext.runAsNonRoot — kubectl explain pod.spec.securityContext.runAsNonRoot · pod.spec.containers.securityContext.runAsUser — kubectl explain pod.spec.containers.securityContext.runAsUser · kind v0.22.0, Kubernetes 1.30.0, containerd 1.7

The scenarios repo has a tiny version of this exact pain.

bash
git clone https://github.com/vellankikoti/troubleshoot-kubernetes-like-a-pro.git cd troubleshoot-kubernetes-like-a-pro/scenarios/security-context-issues ls
bash

description.md, issue.yaml, fix.yaml. The issue manifest sets runAsUser: 0 and runAsGroup: 0 explicitly, which is the honest version of what most teams accidentally do by omission.

Reproduce the issue

bash
kubectl apply -f issue.yaml kubectl get pod security-context-issue-pod
bash
plaintext
NAME READY STATUS RESTARTS AGE security-context-issue-pod 1/1 Running 0 8s

Running. Happy. The pod does not care that it is root. Kubernetes does not care that it is root. The auditor cares very, very much.

Debug the hard way

First question, who is this thing actually running as?

bash
kubectl exec security-context-issue-pod -- id
bash
plaintext
uid=0(root) gid=0(root) groups=0(root)

Root. Inside the container, inside the pod, inside the cluster. Now let me see what the spec actually declares.

bash
kubectl get pod security-context-issue-pod -o jsonpath='{.spec.securityContext}'
bash
plaintext
{"runAsUser":0,"runAsGroup":0}

No runAsNonRoot. No allowPrivilegeEscalation: false. No dropped capabilities. If this container has a bug that lets an attacker execute a shell, they land on a UID 0 shell inside a pod that can talk to the node's kernel with full capabilities, and from there to everything.

bash
kubectl exec security-context-issue-pod -- cat /proc/self/status | grep CapEff
bash
plaintext
CapEff: 00000000a80425fb

That capability mask is the default containerd set, which is still enough to do things like CAP_CHOWN, CAP_SETUID, CAP_NET_RAW. On a root process. In prod.

Why this happens

Container images ship with a USER directive but very few base images set it to a non-root user by default. busybox, alpine, ubuntu, debian, all of them run as UID 0 unless you override it. Kubernetes does not second-guess the image. If you do not set runAsNonRoot: true or runAsUser: <non-zero>, the pod runs as whatever the image says, which is usually root.

The second reason is that securityContext is optional and has sensible-sounding defaults that are not actually secure. allowPrivilegeEscalation defaults to true. readOnlyRootFilesystem defaults to false. capabilities.drop defaults to empty. Every default is the insecure one, because Kubernetes was built for workloads that expected to behave like regular Linux processes. Production hardening is always opt-in.

The third reason is the one nobody wants to admit. Most teams do not set a security context because when they tried, the app broke. Something wrote to a path that was not writable, something tried to bind port 80, something assumed UID 0. So they took it out and promised themselves they would come back. Then two years passed.

The fix

bash
kubectl apply -f fix.yaml kubectl exec security-context-fixed-pod -- id
bash
plaintext
uid=1000 gid=1000 groups=1000

The key diff in the pod spec.

diff
securityContext: - runAsUser: 0 - runAsGroup: 0 + runAsUser: 1000 + runAsGroup: 1000
diff

For real prod hardening I always push teams further than the scenario shows. The minimum baseline I sign off on looks like this.

yaml
securityContext: runAsNonRoot: true runAsUser: 1000 runAsGroup: 1000 fsGroup: 1000 allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: ["ALL"] seccompProfile: type: RuntimeDefault
yaml

runAsNonRoot: true is the one that actually protects you. If the image is accidentally USER root, Kubernetes refuses to start the pod instead of silently running as root. That is the line I want on every workload in every cluster.

The lesson

  1. If you do not set runAsNonRoot: true, assume every pod is running as root. The image will not save you.
  2. The secure defaults are not defaults. allowPrivilegeEscalation: false, capabilities.drop: ["ALL"], readOnlyRootFilesystem: true, seccompProfile: RuntimeDefault. Write them every time.
  3. Hardening that breaks the app is not hardening that shipped. Fix the app. Do not remove the context.

Day 30 of 35 — tomorrow we look at a single line, hostPID: true, that turns a normal pod into a node-wide surveillance deck.

◆ Newsletter

Get the next post in your inbox.

Real Kubernetes lessons from seven years in production. One email when a new post drops. No spam. Unsubscribe in one click.