Containers running as UID 0
What does ZopNight detect here?
A container without runAsNonRoot runs as UID 0 inside the container by default. That is not root on the node, but it removes a meaningful barrier: a container escape starts from root rather than an unprivileged user. ZopNight checks 3 workload kinds.
Signal and threshold
| Field | Value |
|---|---|
| Rule IDs | RC-1723 · RC-1823 · RC-1923 · RC-1724 · RC-1824 · RC-1924 · RC-1725 · RC-1825 · RC-1925 |
| Category | security |
| Severity | high |
| Metric | none — pure configuration read |
| Source | internal/rules/k8s/container_root.go |
Where it applies
Root in a container is not root on the node
Worth stating precisely, because this finding is often over- and under-stated.
UID 0 inside a container is namespaced. Without privileged mode or added capabilities, that process still cannot touch the host directly. The container boundary holds.
What it changes is what happens after a boundary failure. A container-escape vulnerability exploited from UID 0 lands you as root. The same escape from UID 1000 lands you as an unprivileged user who then needs a second privilege-escalation step.
It is defence in depth rather than a boundary in itself, which is why this is worth fixing broadly and rarely worth an emergency.
Why so many images default to root
Because building them that way is easier. Installing packages needs root, binding to ports below
1024 needs root or NET_BIND_SERVICE, and writing to paths created during build needs matching
ownership.
Many official images still run as root unless you override it, so a workload can be running as UID 0 without anyone having chosen that.
The three settings, and the one that actually enforces
securityContext: runAsNonRoot: true # admission REJECTS the pod if the image would run as UID 0 runAsUser: 1000 # sets the UID, but an image can be built to ignore intent allowPrivilegeEscalation: falserunAsNonRoot: true is the enforcing one, failing closed at admission rather than trusting the
manifest. Setting runAsUser alone is a request, not a guarantee.
What breaks when you switch
Two things, predictably: a process binding to port 80 or 443, and a container writing to a path
owned by root. Fix the first by binding above 1024 and letting the Service map the port; fix the
second with fsGroup or by correcting ownership in the image.
Testing runAsNonRoot on the pod securityContext
kubectl get deploy,statefulset,daemonset -A -o json | jq -r ' .items[] | . as $w | .spec.template.spec | select((.securityContext.runAsNonRoot // false) == false) | "\($w.kind) \($w.metadata.namespace)/\($w.metadata.name)"'Enforcing it
Pod Security Admission at restricted requires runAsNonRoot. Apply it to new namespaces first.
Retrofitting it to a namespace full of root-running workloads blocks deploys until every image
is fixed.