Docker audit fix

How to Avoid Privileged Containers in Docker Setups

Running a container with --privileged grants it every Linux capability, drops the seccomp and AppArmor profiles and exposes all host devices, so an escape to the host is trivial. Grant specific --cap-add and --device flags instead.

Hiren KalariyaLast reviewed: Oct 4, 2026Check privileged-containers
High
severity
Yes
needs root or sudo
3
results it can return
1 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

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 thresholds this check applies
ResultWhenWhat it means
SKIPDaemon not reachable / no running containersNothing was verified: either the daemon could not be reached or nothing was running, so a privileged container may still exist.
FAILAny privileged containerAt least one running container holds every capability and every host device, with the seccomp and AppArmor profiles switched off.
PASSNoneEvery 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-add plus --device).
  • The check does not examine what the container actually does with the privilege; it reports the flag.
  • Compose privileged: true sets the same HostConfig.Privileged field, so it is covered. A container given --cap-add ALL without --privileged is 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 true

Work 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/tun

For 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

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