Skip to content

Drop Docker capabilities and set no-new-privileges

Severity: mediumApplies to: Docker Engine 27.x+Applies to: Docker Engine 29.x
The fix
Terminal window
docker run \
--cap-drop=ALL \
--security-opt=no-new-privileges:true \
myimage
services:
app:
cap_drop: [ALL]
security_opt:
- no-new-privileges:true

Add back only what fails without.

A default Docker container isn’t unprivileged — it gets a default set of around fourteen capabilities, whether or not the workload uses any of them. A Node app serving HTTP holds CAP_CHOWN, CAP_SETUID, CAP_SETGID, CAP_NET_RAW, CAP_DAC_OVERRIDE and others, permanently, for no reason.

Each is a tool waiting for an attacker who gets code execution:

  • CAP_DAC_OVERRIDE — bypass file permission checks entirely. Read any file in the container regardless of mode.
  • CAP_SETUID / CAP_SETGID — become any user, which turns a compromise of your unprivileged app user into container root.
  • CAP_NET_RAW — craft raw packets. Enables ARP spoofing against other containers on the same bridge network.
  • CAP_CHOWN — change file ownership, including on bind mounts.

--cap-drop=ALL removes the lot. Most containers need nothing added back, because most containers just run a process and listen on a socket.

no-new-privileges is the cheapest line here

Section titled “no-new-privileges is the cheapest line here”
Terminal window
--security-opt=no-new-privileges:true

This sets the kernel’s no_new_privs bit, which means a process can never gain privileges through execve — setuid binaries stop working as an escalation path.

It matters because dropping capabilities from the container doesn’t stop a setuid binary inside the image from re-elevating. If your image ships sudo, su, ping, mount, or any distro package with a setuid bit, an attacker who lands as your unprivileged user can potentially use it. no_new_privs closes that door for everything at once, and it breaks essentially nothing — no normal application gains privileges through exec.

If you set one flag from this page, set this one.

You can also just remove the setuid bits at build time:

RUN find / -xdev -perm /6000 -type f -exec chmod a-s {} + || true
Need Capability
Bind port < 1024 NET_BIND_SERVICE — but prefer a high port + -p 80:8080
ping NET_RAW — or just don’t ping from production containers
Change file ownership at startup CHOWN — better: fix it at build time
Drop from root to a service user at startup SETUID, SETGID — better: use USER

Note how many entries say “better”. Most capability requirements are a consequence of the container starting as root and then de-escalating — a pattern that USER in the Dockerfile removes entirely. Fix that and the capability list usually collapses to nothing.