Never run containers with --privileged
# Instead of --privileged, grant the one capability you actually needed:docker run --cap-drop=ALL --cap-add=NET_ADMIN myimageservices: app: privileged: false # the default; the finding is when it's true cap_drop: [ALL] cap_add: [NET_BIND_SERVICE]Why it matters
Section titled “Why it matters”--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_MODULEandCAP_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 —
/devfrom 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:
docker run --privileged -it alpine sh# inside:fdisk -l # the host's disks are right theremount /dev/sda1 /mnt # mount the host rootecho '...' >> /mnt/root/.ssh/authorized_keysA 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.
Why people set it
Section titled “Why people set it”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 capability —
NET_ADMINfor a VPN,SYS_TIMEfor NTP,SYS_PTRACEfor a debugger.--privilegedgrants it, along with everything else. - Device access — a serial port, a GPU.
--device=/dev/ttyUSB0grants that one device;--privilegedgrants 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.
Find the capability you actually need
Section titled “Find the capability you actually need”Run the container without --privileged and watch it fail:
docker run --cap-drop=ALL --security-opt seccomp=unconfined myimageIf 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.
Related
Section titled “Related”- Drop capabilities — what to grant instead.
- Container escape — where a privileged container ends up.