Docker audit fix

How to Prevent Privilege Escalation in Docker Runs

Docker containers run without the kernel no_new_privs flag unless you ask for it, so any setuid binary in the image is a route from an unprivileged process to root inside the container. Set --security-opt no-new-privileges:true on every container.

Hiren KalariyaLast reviewed: Oct 4, 2026Check no-new-privileges
High
severity
Yes
needs root or sudo
3
results it can return
2 of 5
checks in this audit

Every threshold on this page is transcribed from the docker-security-container-hardening audit script that CtrlOps runs, and a build check fails if the two ever disagree. See all 5 Container Hardening checks, or how the audit runs.

High severityNeeds sudo

What this check reads

For every id returned by docker ps -q, the check walks the .HostConfig.SecurityOpt array with a Go template and looks for an entry matching no-new-privileges:true or no-new-privileges=true; either spelling counts, but the bare no-new-privileges with no value does not. Containers with no matching entry are collected into a list and counted. Zero containers in that list is a PASS; one or more is a WARN that reports the count as N of M and names the first five.

When it applies

Needs a reachable daemon and at least one running container; otherwise SKIP. It reads .HostConfig.SecurityOpt for each running container and accepts either spelling, no-new-privileges:true or no-new-privileges=true, but not the bare no-new-privileges that Docker also accepts. Only running containers are evaluated. Note the flag is about setuid escalation inside the container - it does not restrict what the container's starting user can already do, so it is a hardening layer, not a substitute for running as non-root.

What each result means

Result thresholds this check applies
ResultWhenWhat it means
SKIPNo running containersNothing was verified: no container config was read, so this says nothing about how your containers are configured.
PASSEvery running container sets itEvery running container carries the flag, so a setuid binary inside one cannot raise privileges.
WARNOne or more lack itThe containers named can still gain privileges through a setuid binary; the count is complete even though only five names are shown.

Why it matters

no_new_privs is a Linux process flag (since kernel 3.5): once set, execve cannot grant more privileges than the parent has, which neutralises setuid/setgid binaries and file capabilities inside the container. Without it, a container process running as a non-root user can still escalate to root through sudo, su, ping or any setuid binary baked into the image. OWASP Docker Cheat Sheet Rule #4; CIS Docker 5.25 "Ensure that the container is restricted from acquiring additional privileges"; Docker docs "Security configuration". The daemon-wide equivalent is "no-new-privileges": true in daemon.json (CIS 2.14).

Why it fails, and when it is wrong

  • The bare form is not recognised. Docker accepts --security-opt no-new-privileges with no value and enforces it, but stores the string exactly as typed, so SecurityOpt holds no-new-privileges and the template, which only compares against the two true spellings, reports the container as missing it. The script's own WARN message recommends --security-opt=no-new-privileges, so following its advice leaves the WARN in place. Use no-new-privileges=true, which Docker and the check both accept.
  • Daemon-wide setting is not detected. If /etc/docker/daemon.json has "no-new-privileges": true, containers get the flag but SecurityOpt does not show it, so the check WARNs incorrectly. Confirm with docker info --format '{{.SecurityOptions}}', which lists name=no-new-privileges when the daemon-wide default is on, or grep NoNewPrivs /proc/<pid>/status for a container PID.
  • Images that legitimately need setuid at startup (gosu/su-exec entrypoints drop privileges down, which still works under no_new_privs; but sudo inside containers breaks).
  • Kubernetes pods are not Docker containers on modern clusters; not applicable.

How to fix it

Find the containers without the flag

docker inspect -f '{{.Name}} {{.HostConfig.SecurityOpt}}' $(docker ps -q) \
  | grep -vE 'no-new-privileges[=:]true'

Every line this prints is a container the audit counts as missing the flag, including any set with the bare no-new-privileges.

Add the flag to each container

docker run --security-opt no-new-privileges=true ...

In Compose, at the service level:

security_opt:
  - no-new-privileges=true

The older no-new-privileges:true spelling also works and also passes the check, but the daemon logs a deprecation warning for the : separator. Avoid the bare no-new-privileges: Docker enforces it, but this check does not recognise it.

Optionally set it daemon-wide

Add "no-new-privileges": true to /etc/docker/daemon.json, then sudo systemctl restart docker, and new containers get the flag by default. The audit cannot see the daemon-wide default, so keep the per-container option as well if you want the finding to clear.

Recreate the containers

security_opt is a HostConfig setting, applied only when a container is created.

docker compose up -d --force-recreate

Containers started with docker run have to be removed and started again with the flag.

Verify the fix

docker inspect -f '{{.Name}} {{.HostConfig.SecurityOpt}}' $(docker ps -q)
# expected: every container lists no-new-privileges=true (or :true)

# Prove it works - setuid binaries can no longer raise privileges:
docker exec <name> sh -c 'id; su -c id' 2>&1
# expected: su fails / no privilege gain

# The kernel's own view for the container's main process:
grep NoNewPrivs /proc/$(docker inspect -f '{{.State.Pid}}' <name>)/status
# expected: NoNewPrivs: 1

Debugging

This one message covers two different cases: the audit could not reach the Docker daemon, or it reached it and nothing was running. Run docker ps as the audit user, then sudo docker ps. If both fail, the daemon is unreachable: check systemctl status docker, then run the audit as root or give the account sudo. If they work but list nothing, there is genuinely nothing to evaluate, and a stopped container is never checked. The Privileged Containers check reports the two cases as separate messages, so its result tells you which one you hit.

The message truncates with head -5 but the count (N of M) is complete. The command under "Find the containers without the flag" prints the full list, and its grep -vE 'no-new-privileges[=:]true' filter keeps the bare form in the list just as the check does.

The flag is on, the check just cannot see it. Docker stores the bare no-new-privileges exactly as typed, and the check only matches no-new-privileges=true or no-new-privileges:true. Confirm the kernel has the flag with the grep NoNewPrivs command above (NoNewPrivs: 1), then recreate the container with --security-opt no-new-privileges=true to clear the finding.

The key is security_opt: [ "no-new-privileges=true" ] at the service level, spelled with =true rather than the bare no-new-privileges, and the container must be recreated (docker compose up -d --force-recreate) for a HostConfig change to apply. Verify with docker inspect, which is exactly what the check reads.

Something inside relies on a setuid binary (sudo, su, ping on older images, a process manager dropping and regaining privileges). Fix the image to start as the right user instead of escalating at runtime; for ping, use --cap-add NET_RAW rather than setuid.

Different problem, different control. no-new-privileges stops gaining privileges; it does nothing about privileges already held. See the Container User check.

Sources

How the script reads this

Next

Re-run the Container Hardening audit after applying the fix and confirm this check moves to PASS. CtrlOps runs all 5 checks over your existing SSH connection and scores the result, so the change is visible without reading another config file.

All 5 Container Hardening fixes
Audit your fleet

Find every one of these on every server, in one click

CtrlOps runs this audit over your existing SSH connection - no agents, no scripts to manage. $7/user/month after a 1 month free trial - no credit card required.

Windows

✓ Start instantly·✓ No credit card·✓ No sneaky autorenewals