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 | When | What it means |
|---|---|---|
| SKIP | No running containers | Nothing was verified: no container config was read, so this says nothing about how your containers are configured. |
| PASS | Every running container sets it | Every running container carries the flag, so a setuid binary inside one cannot raise privileges. |
| WARN | One or more lack it | The 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-privilegeswith no value and enforces it, but stores the string exactly as typed, soSecurityOptholdsno-new-privilegesand the template, which only compares against the twotruespellings, 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. Useno-new-privileges=true, which Docker and the check both accept. - Daemon-wide setting is not detected. If
/etc/docker/daemon.jsonhas"no-new-privileges": true, containers get the flag butSecurityOptdoes not show it, so the check WARNs incorrectly. Confirm withdocker info --format '{{.SecurityOptions}}', which listsname=no-new-privilegeswhen the daemon-wide default is on, orgrep NoNewPrivs /proc/<pid>/statusfor a container PID. - Images that legitimately need setuid at startup (
gosu/su-execentrypoints drop privileges down, which still works under no_new_privs; butsudoinside 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=trueThe 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-recreateContainers 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: 1Debugging
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
- Docker: docker run
--security-opt(no-new-privileges) - Docker: dockerd daemon.json
no-new-privileges - Linux kernel: No New Privileges Flag
- OWASP Docker Security Cheat Sheet (Rule #4)
- Docker Compose: security_opt
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