Docker & ContainersXI · Container & Host SecurityPrivileged containers
Privileged containers and the security boundary
What you'll learn
- Explain precisely what --privileged does, control by control
- Identify the (very narrow) legitimate use cases
- Choose alternatives for common patterns
- Migrate a privileged container to an explicit capability and device list
Prerequisites
None — start here.
Verified against Docker Engine 29.x · Docker Engine 28.x · Docker Compose 2.x · containerd 2.x · runc 1.2.x · BuildKit 0.20+ · Linux kernel 5.15+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-09
--privileged is not “a container with more capabilities”. It is a
single flag that turns off five independent security mechanisms at
once, and the reason it is so dangerous is that four of them are not
mentioned in its name.
What --privileged actually switches off
| Mechanism | Normal container | With --privileged |
|---|---|---|
| Capabilities | 14 in the bounding set | every capability the kernel defines |
| Device cgroup | a short allowlist (null, zero, full, random, urandom, tty, console, ptmx) | every device on the host, rwm |
| seccomp | the default profile, blocking roughly 44 syscalls | unconfined |
| AppArmor / SELinux | docker-default / container_t | unconfined |
/proc and /sys | 12 masked paths, 5 read-only paths, /sys mounted ro, cgroups ro | masks removed, read-only lists cleared, /sys and the cgroup mount writable |
Any one of those five is a plausible route out of the container. Together they mean a privileged container is a root shell on the host with a couple of extra steps.
The escalation does not need a vulnerability. With CAP_SYS_ADMIN
in hand and every device node available, a process inside the
container can mount the host’s root block device onto a directory
inside its own mount namespace and then read and write the host’s
filesystem directly — placing an authorised key, a systemd unit, or
a setuid binary. The container never “escaped”; it was handed the
tools to reach around the boundary.
Proving it, from the host
$ PID=$(docker inspect --format '{{.State.Pid}}' dind); grep -E 'CapBnd|CapEff' "/proc/$PID/status"; cat /proc/sys/kernel/cap_last_capCapBnd: 000001ffffffffff
CapEff: 000001ffffffffff
40Illustrative output
A CapBnd of all-ones is the fingerprint. 40 is
CAP_CHECKPOINT_RESTORE, the highest capability on a current
kernel, which is why the mask has 41 bits set. On an older kernel
the constant differs — which is exactly why the audit should compare
CapBnd against Docker’s default 00000000a80425fb and flag
anything larger, rather than looking for one magic value.
docker ps -q | while read -r c; do
docker inspect --format '{{if .HostConfig.Privileged}}PRIVILEGED {{.Name}} {{.Config.Image}}{{end}}' "$c"
done | grep .CID=dind
docker inspect "$CID" --format 'apparmor={{.AppArmorProfile}} privileged={{.HostConfig.Privileged}}'
PID=$(docker inspect --format '{{.State.Pid}}' "$CID")
cat "/proc/$PID/attr/current"
grep Seccomp "/proc/$PID/status"Seccomp: 0 means no filter is loaded. Seccomp: 2 is filter mode,
which is what a normal container shows. That single line is the
cleanest evidence in a report, because it comes from the kernel and
nobody can argue with it.
The legitimate use cases, honestly
Docker’s documentation does not publish a list of sanctioned uses, so be sceptical of any source that claims “the docs say two”. In practice the cases that come up:
- Docker-in-Docker. A daemon inside a container needs to create namespaces, mount overlayfs, and manage cgroups. This is the common one, and it has good alternatives (below).
- System containers that manage host hardware — a storage
agent that must run
mkfson a raw device, a firmware updater, a driver installer. These are host software packaged as a container, not workloads. - Kernel and filesystem development. Legitimate, and by definition not production.
In every one of these, the container is trusted exactly as much as
the host. That is the test. If you would not give this code root
over SSH, it does not get --privileged, and reasoning about the
image’s provenance is a supply-chain question, not a runtime one.
Alternatives, by what you were actually trying to do
You reached for --privileged because… | Do this instead |
|---|---|
| the container needs one device | --device /dev/ttyUSB0:/dev/ttyUSB0 (or renderD128, or nvidia via the container toolkit) |
| it needs to mount something | mount it on the host and bind-mount the result; or --cap-add=SYS_ADMIN with a narrow, reviewed justification |
| it needs to bind a low port | --cap-add=NET_BIND_SERVICE, or publish a high port and map it |
| it needs to change network settings | --cap-add=NET_ADMIN, scoped to its own netns |
| it needs to load a kernel module | load it on the host, from host package management. A container cannot own the host kernel. |
it needs tcpdump/ping | --cap-add=NET_RAW (which is in the default set already) |
| it needs to adjust process priorities | --cap-add=SYS_NICE |
it needs strace/a debugger | --cap-add=SYS_PTRACE, and only in the debug container |
| it needs the Docker API | a filtering socket proxy, not a privileged container. See the Docker socket lesson. |
| it builds images in CI | rootless BuildKit, or a build tool that does not need a daemon |
Notice how many rows are a single --cap-add. That is the usual
truth: the workload needed one capability and somebody reached for
all forty.
The Docker-in-Docker case
services:
# Option 1: privileged. The inner daemon runs as host root.
dind:
image: docker:28-dind
privileged: true
# Option 2: rootless. The inner daemon runs as an unprivileged
# user, so an escape lands on an unprivileged host UID.
dind-rootless:
image: docker:28-dind-rootless
security_opt:
- seccomp:unconfined
- apparmor:unconfined
Option 2 is strictly safer and is what the docker:dind-rootless
image exists for. It is not free: it needs seccomp and apparmor
set to unconfined so the inner runtime can create nested user
namespaces, which reads alarming in a review and is a real
reduction — but a much smaller one than granting every capability
and every device.
For production CI the stronger answers are outside Docker’s model entirely: a disposable VM per job, a nested-container runtime such as sysbox, or an image builder that never needs a daemon. Pick based on how much you trust the code in the pull request, which for a public repository is “not at all”.
Knowledge check
Knowledge check · 5 questions
Q1. Which of these does `--privileged` change? Select all that apply.
Q2. What is the effect of `docker run --privileged --cap-drop=ALL`?
Q3. cgroup resource limits such as `--memory` and `--cpus` still apply to a `--privileged` container.
Q4. A container needs to run `tcpdump` for a network investigation. What is the minimal grant?
Q5. Which line in /proc/PID/status distinguishes a container running with the default seccomp profile from one running unconfined?
Passing score: 75%. Answers are checked in this browser.