Why Running Containers as Root Is Still the #1 Mistake
A container is not a security boundary in the way a virtual machine is. It is a process on the host kernel wearing a set of namespaces and cgroups. So when that process runs as root — which, by default, it does — you are one kernel bug or one misconfiguration away from root on the host. Running as root remains the most common container hardening failure because nothing forces you to fix it, and everything works fine until it doesn't.
When you write a Dockerfile with no USER instruction, the resulting container runs its main process as UID 0. That UID is the same UID 0 the kernel knows on the host; user namespaces, which would remap it, are off by default in most runtimes. Inside the container the process has a restricted capability set, but 'restricted' is not 'none', and the attack surface it shares with every other tenant is the host kernel itself.
What root inside a container actually buys an attacker
Suppose an attacker achieves remote code execution in your application — a deserialization bug, an SSRF that reaches an internal admin endpoint, whatever. If the process is root, they inherit its capabilities. They can write to any bind-mounted host path the container was granted, read secrets mounted into the filesystem, install tooling, and probe for a kernel or runtime flaw to break out entirely. Escapes such as the 2019 runc overwrite (CVE-2019-5736) and the 2024 'Leaky Vessels' runc flaw (CVE-2024-21626) were dramatically easier to weaponise from a root process than from an unprivileged one.
Run the same process as an unprivileged UID and most of that collapses. The attacker cannot write to root-owned host mounts, cannot bind low ports, and hits permission denials on the very syscalls an escape needs. It is not a guarantee, but it removes the cheapest paths first.
Fixing it in the image and at admission
- Set a numeric
USERin the Dockerfile. Prefer a high, explicit UID (e.g.USER 10001) over a username, so admission policies that checkrunAsNonRootcan evaluate it without resolving/etc/passwd. - Enforce it in the pod spec. Set
securityContext.runAsNonRoot: trueand a concreterunAsUser; the kubelet will refuse to start a container that tries to run as UID 0. - Drop capabilities to the floor. Add
capabilities.drop: ["ALL"]and add back only what the workload provably needs. Most web services need none. - Make the root filesystem read-only.
readOnlyRootFilesystem: trueplus explicitemptyDirwritable mounts stops an attacker planting a binary where the app will re-execute it. - Block privilege escalation.
allowPrivilegeEscalation: falseprevents a setuid binary inside the image from regaining capabilities the runtime dropped.
RUN adduser -D -u 10001 app
USER 10001
# pod spec
securityContext:
runAsNonRoot: true
runAsUser: 10001
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: [ "ALL" ] }
Why it persists
The reason root-by-default survives is friction, not ignorance. A base image expects to write to /var; an init script wants to chown; a legacy service binds port 80. Each is solvable — pre-create writable dirs, bind a high port and remap it at the service, use file capabilities instead of full root — but each takes a few minutes, and 'it works' is a powerful anaesthetic. The fix is to make non-root the default your platform ships, so every new image inherits it and opting back into root is the exception that gets reviewed.
None of this matters for the workloads you don't know are running. A team that spun up a container on a forgotten node, as root, with a host mount, is exactly the blind spot an attacker finds first — and you can't harden what you haven't discovered.
- Pick a few running containers and check the effective UID of the main process (
docker inspect, or the pod'ssecurityContext/idinside the container). Anything sitting at UID 0 is running as root and worth flagging. - Skim your Dockerfiles for a missing
USERline — noUSERmeans the image defaults to root.
Spot-checking a handful proves the point; enumerating every workload across every cluster and ranking the real blast radius is what our assessment runs for you.