Skip to main content
RunBook Academy

Docker & ContainersXI · Container & Host SecurityPrivileged containers

Privileged containers and the security boundary

Advanced⏱ ~24 min

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

Not yet marked complete on this device.

--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

MechanismNormal containerWith --privileged
Capabilities14 in the bounding setevery capability the kernel defines
Device cgroupa short allowlist (null, zero, full, random, urandom, tty, console, ptmx)every device on the host, rwm
seccompthe default profile, blocking roughly 44 syscallsunconfined
AppArmor / SELinuxdocker-default / container_tunconfined
/proc and /sys12 masked paths, 5 read-only paths, /sys mounted ro, cgroups romasks 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

Read-only / Safethe capability set of a privileged container
$ PID=$(docker inspect --format '{{.State.Pid}}' dind); grep -E 'CapBnd|CapEff' "/proc/$PID/status"; cat /proc/sys/kernel/cap_last_cap
CapBnd:	000001ffffffffff
CapEff:	000001ffffffffff
40

Illustrative 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.

Read-only / Safefind every privileged container on the host
docker ps -q | while read -r c; do
  docker inspect --format '{{if .HostConfig.Privileged}}PRIVILEGED {{.Name}} {{.Config.Image}}{{end}}' "$c"
done | grep .
Read-only / Safeconfirm the confinement is really gone
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:

  1. 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).
  2. System containers that manage host hardware — a storage agent that must run mkfs on a raw device, a firmware updater, a driver installer. These are host software packaged as a container, not workloads.
  3. 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 somethingmount 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 moduleload 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 APIa filtering socket proxy, not a privileged container. See the Docker socket lesson.
it builds images in CIrootless 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

  1. Q1. Which of these does `--privileged` change? Select all that apply.

  2. Q2. What is the effect of `docker run --privileged --cap-drop=ALL`?

  3. Q3. cgroup resource limits such as `--memory` and `--cpus` still apply to a `--privileged` container.

  4. Q4. A container needs to run `tcpdump` for a network investigation. What is the minimal grant?

  5. 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.