Skip to content

Never run containers with --privileged

Severity: highApplies to: Docker Engine 27.x+Applies to: Docker Engine 29.x
The fix
Terminal window
# Instead of --privileged, grant the one capability you actually needed:
docker run --cap-drop=ALL --cap-add=NET_ADMIN myimage
docker-compose.yml
services:
app:
privileged: false # the default; the finding is when it's true
cap_drop: [ALL]
cap_add: [NET_BIND_SERVICE]

--privileged is not “a few more permissions”. It turns off, simultaneously:

  • All capability restrictions — the container gets every capability, including CAP_SYS_ADMIN, CAP_SYS_MODULE and CAP_SYS_PTRACE.
  • The seccomp profile — every syscall becomes reachable, including the ones Docker’s default profile blocks specifically because they enable escapes.
  • AppArmor / SELinux confinement — the container runs unconfined.
  • Device restrictions/dev from the host is exposed, so the container can see raw block devices.

That last one is usually the end of the story. With host devices visible, a privileged container mounts the host’s root filesystem and writes to it:

Terminal window
docker run --privileged -it alpine sh
# inside:
fdisk -l # the host's disks are right there
mount /dev/sda1 /mnt # mount the host root
echo '...' >> /mnt/root/.ssh/authorized_keys

A privileged container is not isolated from the host in any meaningful way. It is a process running as root with a different filesystem view, and it can drop even that.

Nobody reaches for --privileged maliciously. It’s what makes the error stop:

  • Docker-in-Docker for CI. Genuinely needs a lot — but the modern answer is a rootless builder like BuildKit or Kaniko, not a privileged daemon.
  • A container that needs one capabilityNET_ADMIN for a VPN, SYS_TIME for NTP, SYS_PTRACE for a debugger. --privileged grants it, along with everything else.
  • Device access — a serial port, a GPU. --device=/dev/ttyUSB0 grants that one device; --privileged grants all of them.
  • Something failed with “operation not permitted” and this made it stop.

In nearly every case the fix is a specific flag. The work is finding out which one, and that’s a five-minute exercise rather than an architectural problem.

Run the container without --privileged and watch it fail:

Terminal window
docker run --cap-drop=ALL --security-opt seccomp=unconfined myimage

If it now works, you needed a capability, not privilege. Add them back one at a time — strace inside the container names the syscall that failed, and that maps to a capability. Common ones:

Need Flag
Bind to port < 1024 --cap-add=NET_BIND_SERVICE
Configure interfaces, VPN, iptables --cap-add=NET_ADMIN
Raw sockets, ping --cap-add=NET_RAW
A specific device --device=/dev/ttyUSB0
Set the system clock --cap-add=SYS_TIME
Mount filesystems, FUSE --cap-add=SYS_ADMIN — treat this as privileged

CAP_SYS_ADMIN is worth its own warning: it is so broad it is sometimes called “the new root”. Granting it is not a meaningful improvement on --privileged — if you find you need it, the design is what needs revisiting.