What this check reads
For each running container the check reads .Config.User from docker inspect and treats the values '', 0, root, 0:* and root:* as root. It also runs docker info --format '{{.SecurityOptions}}' once and counts how many lines mention userns. If no container matches the root patterns the result is a PASS; if any do, the presence of userns in the daemon's security options is what decides between WARN and FAIL.
When it applies
Needs a reachable daemon and at least one running container; otherwise SKIP. It reads .Config.User for each running container - the declared user, from the image's USER directive or -u/user: - and treats empty, 0, root, 0:* and root:* as root. It does not look at what the process is actually running as now: an image that starts as root and drops privileges internally (nginx master, many entrypoint scripts) is still reported as root. The severity depends on docker info reporting userns in its security options: with user-namespace remapping active the result softens from FAIL to WARN, because container root maps to an unprivileged host UID.
What each result means
| Result | When | What it means |
|---|---|---|
| SKIP | No running containers | Nothing was verified: no container config was read, so treat the user each container runs as unknown. |
| PASS | Every container declares a non-root user | Every running container declares a non-root user, though that is the declared user and not necessarily the process that ends up running. |
| WARN | Root containers exist but userns-remap is active | Some containers still run as root, but remapping means container root is an unprivileged UID on the host. |
| FAIL | Root containers, no user namespace | The containers named run as UID 0 with no remapping, so container root is host root the moment one of them escapes. |
Why it matters
Container root is host root (UID 0) unless user namespaces remap it. An escape from a root container (kernel bug, misconfigured mount, runc CVE) lands as root on the host. OWASP Docker Cheat Sheet Rule #2 "Set a user"; CIS Docker 4.1 "Ensure that a user for the container has been created" and 5.x; NIST SP 800-190 4.4.3. Docker's own guidance: use the USER instruction or --user.
Why it fails, and when it is wrong
- Official images that start as root and drop privileges.
nginx,postgres,mysql,redis,mongo,rabbitmqand many others have.Config.Userempty because the entrypoint runs as root to fix permissions and thengosu/setprivto the service user. The workers are unprivileged but the check sees the image config and reports FAIL. Check reality withdocker top <name>ordocker exec <name> ps -o user,comm. Mitigate with--userwhere the image supports it (nginx-unprivileged variant,postgreswith--user postgresand pre-created volume ownership), or accept and document. .Config.Useris the image/container config, not the runtime effective user; a DockerfileUSER 1000sets it to1000(PASS).- Rootless Docker remaps every container, including root ones, but
SecurityOptionsshowsrootlessnotuserns; the check would FAIL instead of WARN on rootless hosts. Cross-check with the Rootless Mode result.
How to fix it
Set a non-root USER in the image
RUN addgroup --system app && adduser --system --ingroup app app
USER appThat is Debian and Ubuntu syntax. On RHEL and UBI images use groupadd --system app && useradd --system --gid app app, and on Alpine addgroup -S app && adduser -S -G app app. Where the vendor publishes an unprivileged variant, use it rather than building your own: nginxinc/nginx-unprivileged, distroless :nonroot, Bitnami images.
Or set the user at run time
When you cannot change the image:
docker run --user 1000:1000 ...In Compose, user: "1000:1000". Images whose entrypoint expects to start as root, fix permissions and then drop privileges usually need their volume ownership prepared first, as in the next step.
Match volume ownership and ports
A non-root process cannot write to a volume owned by root, or bind a port below 1024.
sudo chown -R 1000:1000 ./dataHave the process listen on a high port inside the container and publish it on the low one, for example -p 80:8080.
Rebuild and recreate
docker compose up -d --build --force-recreate.Config.User is fixed when the container is created, so a rebuilt image changes nothing until the container is recreated from it.
Verify the fix
docker inspect -f '{{.Name}} user="{{.Config.User}}"' $(docker ps -q)
# expected: a non-root user or UID for every container
# What the processes really run as inside:
docker exec <name> id
docker top <name> # host-side view of the same processes
# Is user-namespace remapping on? (turns the FAIL into a WARN)
docker info --format '{{.SecurityOptions}}'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 check reads the declared user, not the running one. docker top <name> and docker exec <name> id show the truth. A master process that must start as root (binding port 80, for example) is a legitimate exception; the durable fix is to publish an unprivileged port and set USER in the image, after which the finding clears honestly.
User-namespace remapping is enabled, so container root is an unprivileged host UID. Good, but a USER directive is still better: remapping does not protect against everything, and one --userns=host container bypasses it.
The container was started with an explicit override (-u 0, Compose user: root), or you rebuilt the image without recreating the container. Check docker inspect -f '{{.Config.User}}' against docker image inspect -f '{{.Config.User}}' <image>.
Usually file ownership on mounted volumes or a bind to a port below 1024. Fix ownership (chown -R uid:gid on the volume, or --user matching the host owner) and map a high port instead. See the Volume Ownership check.
Sources
- Docker: Dockerfile USER
- Docker: docker run
--user - Docker: Isolate containers with a user namespace
- OWASP Docker Security Cheat Sheet (Rule #2)
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