Capabilities and the Root Illusion
A container runs as root. Is that dangerous? The honest answer is 'it depends entirely on capabilities' and most engineers can't explain why.
We have covered what a container can see (namespaces) and what it can use (cgroups). This lesson is about what it can do: privilege. The reason "runs as root" is not a yes-or-no security question is that on modern Linux, root is not one thing. The kernel long ago split the historic all-powerful UID 0 into around forty discrete capabilities, each a specific privilege that can be granted or removed independently. What a process can do is decided by which capabilities it holds, not by its UID.
"Root" is a set of capabilities, not a magic UID. UID 0 with an empty capability set can do almost nothing privileged; a non-zero UID that holds the right capability can perform that privileged operation. When you ask "is this container dangerous because it runs as root?" the real question is always "which capabilities does it hold?"
Root, decomposed
Each capability names one slice of former root power. A handful of the security-relevant ones:
| Capability | Grants the power to |
|---|---|
CAP_SYS_ADMIN | Mount filesystems, manipulate namespaces, and a huge grab-bag of admin operations |
CAP_NET_ADMIN | Reconfigure interfaces, routes, and firewall rules |
CAP_NET_RAW | Craft raw packets (ping, but also spoofing and sniffing) |
CAP_SYS_PTRACE | Trace and inspect other processes' memory |
CAP_SYS_MODULE | Load and unload kernel modules (game over if held) |
CAP_DAC_OVERRIDE | Bypass file read/write/execute permission checks |
CAP_SETUID / CAP_SETGID | Change process UID/GID arbitrarily |
A container runtime does not give a container all of them. Docker and containerd start containers with a default bounding set of roughly 14 capabilities and drop the rest. So the default container root already lacks CAP_SYS_ADMIN, CAP_SYS_MODULE, CAP_SYS_PTRACE, and most of the dangerous ones. That default is why running as root in a normal container is far less alarming than running as root on a host.
Inspecting and dropping capabilities
You can see the exact set a container holds and tighten it:
# The default set a container gets
docker run --rm alpine sh -c 'apk add -q libcap; capsh --print' | grep Current
# Current: cap_chown,cap_dac_override,cap_fowner,...,cap_net_bind_service,...
# Best practice: drop everything, add back only what the app needs
docker run --rm --cap-drop=ALL --cap-add=NET_BIND_SERVICE nginx
# nginx can bind port 80 as non-root, and can do nothing else privileged
In Kubernetes the same thing is securityContext.capabilities.drop: ["ALL"] with an explicit add list. The discipline is identical: start from nothing, grant the minimum.
root vs privileged: not the same thing
These two are constantly confused, and the difference is the whole lesson.
Default root container vs a privileged container
Runs as root (default container)
UID 0 inside, but heavily bounded
--privileged
Effectively host root
"Runs as root" means UID 0 with the default, trimmed capability set. --privileged means every capability, all host devices, and no seccomp confinement. A privileged container is not "a bit more powerful," it is effectively root on the host and one of the most common real-world escape paths (Module 4). The word "privileged" is doing enormous work; never grant it casually.
CAP_SYS_ADMIN: the dangerous one
If a single capability deserves a warning, it is CAP_SYS_ADMIN. It is so overloaded that it has been called "the new root": it grants mounting, namespace manipulation, and dozens of unrelated admin operations. Many container escapes require exactly this one capability, because with it a process can mount filesystems or manipulate namespaces in ways that reach the host. If a workload asks for CAP_SYS_ADMIN, treat that as a request for host-level trust and push back hard.
no-new-privileges
One more control ties it together. The no-new-privileges flag (the PR_SET_NO_NEW_PRIVS prctl) prevents a process and its children from gaining privileges later, for example through a setuid binary or a file with file capabilities. Even if an attacker drops a setuid-root helper into the container, it cannot use it to escalate. In Kubernetes this is securityContext.allowPrivilegeEscalation: false. It is cheap and should be the default for every workload.
docker run --rm --security-opt no-new-privileges alpine \
sh -c 'id; echo "cannot escalate via setuid binaries"'
Common mistakes
- Equating UID 0 with danger. Root with an empty capability set is nearly harmless; a non-root UID with a powerful capability is not. Judge by capabilities, not UID.
- Reaching for
--privilegedto fix a permission error. It almost always grants far more than the one missing capability. Find the specific capability (or device, or mount) and grant only that. - Leaving the default capability set in place for sensitive workloads. The default ~14 is convenient, not minimal. Drop ALL and add back what the app provably needs.
- Granting
CAP_SYS_ADMINto make something work. It is the widest capability there is and a frequent escape enabler. If an app needs it, question the design. - Forgetting
no-new-privileges. Without it, a setuid binary in the image can undo your careful capability dropping.
The interview answer
The scenario's exact question: root with all capabilities dropped, versus an unprivileged UID holding CAP_SYS_ADMIN. The second is far more dangerous. Capabilities, not the UID, decide what a process can do, and the kernel checks the capability regardless of whether the UID is 0. Root-with-no-capabilities can open its own files and little else privileged; it cannot mount, load modules, or manipulate namespaces. The unprivileged-UID-with-CAP_SYS_ADMIN process can perform the mount and namespace operations that lead to container escapes, precisely because it holds the capability that authorizes them. The lesson: stop reading the UID and start reading the capability set.
Summary
Root is a bundle of around forty capabilities, and the kernel authorizes privileged actions by capability, not by UID. Container runtimes grant a trimmed default set (about 14) and drop the rest, which is why default container-root is far weaker than host-root. --privileged restores everything and is effectively host root; CAP_SYS_ADMIN is the single most dangerous capability and a common escape enabler; no-new-privileges stops later escalation. Best practice is drop ALL, add the minimum, never go privileged, and set no-new-privileges. Next: seccomp and the LSMs that constrain a process even further, at the syscall level.
Container A runs as root (UID 0) with all capabilities dropped and no-new-privileges set. Container B runs as an unprivileged user (UID 1000) but holds CAP_SYS_ADMIN. Which is more dangerous, and what principle explains it?
A container runs as root but with all capabilities dropped. Another runs as an unprivileged user but with CAP_SYS_ADMIN. Which is more dangerous and why?