What this check reads
The check gathers the running container ids from docker ps -q, then runs docker inspect --format '{{if .HostConfig.Privileged}}{{.Name}}{{end}}' <ids> over all of them in one call. The template emits a container's name only when the boolean .HostConfig.Privileged is true, which is what --privileged on docker run or privileged: true in Compose sets. If any name survives in that list once whitespace is stripped the check FAILs and prints the names; an empty list PASSes and reports how many running containers were examined.
When it applies
Requires the Docker daemon to be reachable - as root, through sudo, or as a member of the docker group - and at least one running container; otherwise SKIP. It inspects docker ps -q only, so stopped containers, and images that would run privileged when started, are never evaluated. A Compose service defined with privileged: true but not currently up will not be reported. Podman containers are not audited even though podman is detected by the shared preamble.
What each result means
| Result | When | What it means |
|---|---|---|
| SKIP | Daemon not reachable / no running containers | Nothing was verified: either the daemon could not be reached or nothing was running, so a privileged container may still exist. |
| FAIL | Any privileged container | At least one running container holds every capability and every host device, with the seccomp and AppArmor profiles switched off. |
| PASS | None | Every container running at scan time had the privileged flag off. |
Why it matters
--privileged disables almost every isolation layer: all capabilities are granted, the seccomp and AppArmor profiles are dropped, all host devices are exposed under /dev, and the container can mount the host filesystem, load kernel modules, or write to /sys. Escape is trivial (mount the host disk, or abuse cgroups release_agent). OWASP Docker Security Cheat Sheet Rule #3 "Limit capabilities (Grant only specific capabilities, needed by a container)" explicitly says never to run containers with --privileged. CIS Docker Benchmark 5.4 "Ensure that privileged containers are not used". NIST SP 800-190 section 4.4.
Why it fails, and when it is wrong
- Common culprits: Docker-in-Docker (
docker:dind), Portainer agents, some monitoring agents, Tailscale/VPN containers, Home Assistant addons, CI runners. Many of these have documented non-privileged alternatives (rootless DinD, Sysbox, specific--cap-addplus--device). - The check does not examine what the container actually does with the privilege; it reports the flag.
- Compose
privileged: truesets the sameHostConfig.Privilegedfield, so it is covered. A container given--cap-add ALLwithout--privilegedis not flagged here; the Container Capabilities check catches that one.
How to fix it
Find the privileged containers
The audit only sees running containers, so include the stopped ones here.
docker inspect -f '{{.Name}} {{.HostConfig.Privileged}}' $(docker ps -aq) | grep trueWork out what each one actually needs
--privileged is usually standing in for one narrower grant, so read the image's documentation before you remove it. VPN and WireGuard containers typically need NET_ADMIN and /dev/net/tun, some agents need a single --device, and time-sync containers need SYS_TIME. For Docker-in-Docker, use rootless DinD, or build images with BuildKit, kaniko or buildah, none of which need the flag.
Replace --privileged with specific grants
docker run --cap-drop ALL --cap-add NET_ADMIN --device /dev/net/tun ...In Compose, remove privileged: true and grant the same things explicitly:
services:
vpn:
cap_drop: [ALL]
cap_add: [NET_ADMIN]
devices:
- /dev/net/tunFor access to a class of devices rather than one node, use --device-cgroup-rule instead of going back to --privileged.
Recreate the container
privileged is a HostConfig setting, applied only when the container is created.
docker compose up -d --force-recreate <service>If nothing narrower works, move the workload to a dedicated VM and record the exception.
Verify the fix
# No running container should be privileged:
docker inspect -f '{{.Name}} {{.HostConfig.Privileged}}' $(docker ps -q)
# expected: false for every container
# Check the definitions too, not just what is running:
grep -rn 'privileged' docker-compose.yml compose.yaml 2>/dev/null
# Confirm the replacement actually has what it needs:
docker inspect -f '{{.Name}} caps={{.HostConfig.CapAdd}} devices={{.HostConfig.Devices}}' <name>Debugging
The audit account cannot talk to the daemon. Test with docker ps; if that fails, run the audit as root, give the account sudo, or add it to the docker group (usermod -aG docker <user>, then re-login). Note that docker group membership is equivalent to root on the host - see the Docker Group Members check.
Genuinely nothing to evaluate. This is not a pass: a privileged container that is currently stopped will not be reported. Check definitions with docker ps -a and the grep above.
Find out what it actually uses before removing the flag: docker inspect -f '{{.HostConfig.Devices}} {{.HostConfig.CapAdd}}' <name>, and watch for permission errors after switching to --cap-drop ALL --cap-add <specific>. Common real needs are NET_ADMIN (VPN/tun), SYS_TIME, or a specific --device.
The usual reason for --privileged. Use rootless DinD, or build images with BuildKit/kaniko/buildah, which do not need it.
The running container still has the old config. docker compose up -d --force-recreate <service>; docker inspect shows what the running container actually has, which is what the check reads.
Sources
- Docker: docker run
--privileged - Docker: Runtime privilege and Linux capabilities
- OWASP Docker Security Cheat Sheet
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