cap-drop ALL & no-new-privileges
Least privilege, actually enforced.
Even a non-root container starts with a default set of ~14 capabilities, most of which a typical app never uses — and any of them is a rung an attacker might climb. Least privilege here is mechanical: drop ALL capabilities, then add back only the specific ones a proven need requires (often none, sometimes just NET_BIND_SERVICE). Pair it with no-new-privileges so the process can never gain capabilities it did not start with.
$ docker run -d \--cap-drop ALL \ # start from zero capabilities--cap-add NET_BIND_SERVICE \ # add back ONLY if it binds a port < 1024--security-opt no-new-privileges \ # block setuid/privilege escalation--user 10001:10001 \myapp:1.0
no-new-privileges closes the setuid door
no-new-privileges sets a kernel flag that prevents a process (and its children) from ever gaining more privileges than it has — so a setuid-root binary inside the container cannot elevate, and known privilege-escalation tricks that rely on setuid are dead on arrival. It costs nothing and shuts a whole class of escalation, which is why it belongs on essentially every container alongside the capability drop.
# with no-new-privileges, a setuid-root helper cannot elevate$ docker run --rm --security-opt no-new-privileges alpine \sh -c 'ls -l /bin/su; su -c id 2>&1 | tail -1'su: must be run from a terminal # (and it cannot gain the setuid privilege)
Find the real minimum
Do not guess which capabilities an app needs — drop ALL, run it, and add back only what actually fails. Most services need none; the common exceptions are NET_BIND_SERVICE (bind a low port) and, rarely, CHOWN/SETUID/SETGID during an entrypoint that drops privileges. If an app claims it needs SYS_ADMIN or NET_ADMIN, treat that as a design smell to investigate, not a flag to add.