Docker audit fix

How to Drop Unnecessary Docker Linux Capabilities

Docker gives every container 14 Linux capabilities by default, and adding SYS_ADMIN or NET_ADMIN puts it within reach of root on the host. Set --cap-drop ALL on every container and re-add only the specific capabilities its workload fails without.

Hiren KalariyaLast reviewed: Oct 4, 2026Check capabilities
Medium
severity
Yes
needs root or sudo
4
results it can return
4 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.

Medium severityNeeds sudo

What this check reads

For each running container the check inspects two fields, .HostConfig.CapAdd and .HostConfig.CapDrop. A container whose CapAdd matches ALL, SYS_ADMIN or NET_ADMIN on a case-insensitive grep goes into the ADDALL list, and a container whose CapDrop is empty, meaning [], <no value> or blank, goes into the NODROP list. The two lists are applied in priority order: a non-empty ADDALL is a FAIL, otherwise a non-empty NODROP is a WARN, and only when both are empty does the check PASS.

When it applies

Needs a reachable daemon and at least one running container; otherwise SKIP. It reads two fields per running container, .HostConfig.CapAdd and .HostConfig.CapDrop, and applies them in priority order: adding ALL, SYS_ADMIN or NET_ADMIN (case-insensitive substring match) is a FAIL; otherwise an empty CapDrop - meaning the container keeps Docker's full default capability set - is a WARN. Note the consequences of that logic: any non-empty CapDrop satisfies the PASS branch, so dropping a single harmless capability passes just as well as --cap-drop ALL. Other dangerous additions (SYS_PTRACE, SYS_MODULE, DAC_READ_SEARCH) are not in the FAIL list and only avoid a finding if something was dropped.

What each result means

Result thresholds this check applies
ResultWhenWhat it means
SKIPNo running containersNothing was verified: no capability set was read, so no container has been cleared.
FAILDangerous capability addedA running container was granted ALL, SYS_ADMIN or NET_ADMIN, which puts it within reach of root on the host.
WARNSome containers keep the full default set (no `--cap-drop`)The containers named still hold Docker's 14 default capabilities because nothing was dropped.
PASSEvery container drops capabilitiesEvery running container drops at least one capability, which is not the same as dropping all of them.

Why it matters

Docker grants 14 capabilities by default (CHOWN, DAC_OVERRIDE, FSETID, FOWNER, MKNOD, NET_RAW, SETGID, SETUID, SETFCAP, SETPCAP, NET_BIND_SERVICE, SYS_CHROOT, KILL, AUDIT_WRITE). Most services need none of them as a non-root user. CAP_SYS_ADMIN is "the new root" (mounts, namespaces, BPF on older kernels); CAP_NET_ADMIN allows interface/iptables manipulation and sniffing; CAP_NET_RAW (default) enables ARP spoofing between containers. OWASP Docker Cheat Sheet Rule #3 "Limit capabilities"; CIS Docker 5.3 "Ensure that Linux kernel capabilities are restricted within containers"; capabilities(7).

Why it fails, and when it is wrong

  • Legitimate NET_ADMIN users: VPN/WireGuard containers, Pi-hole in some modes, network tools. SYS_ADMIN: FUSE mounts, some backup agents, DinD. Each should be paired with --cap-drop ALL and only the named capability.
  • The WARN for "no --cap-drop" fires on practically every default deployment. It is advisory; prioritise --cap-drop ALL on internet-facing containers first.
  • --privileged implies all capabilities but CapAdd stays empty; that case is covered by the Privileged Containers check.
  • CapDrop containing anything non-empty (even a single NET_RAW) counts as "drops capabilities".

How to fix it

Start from --cap-drop ALL

docker run --cap-drop ALL ...

In Compose:

cap_drop: [ALL]

Add back only what fails

Run the workload and watch the logs for EPERM or "Operation not permitted". Each failure points at a capability to add back, so add them one at a time. Typical needs are CHOWN, SETUID, SETGID and NET_BIND_SERVICE, and capsh --print or pscap inside a working copy shows the current set as a starting point.

docker run --cap-drop ALL --cap-add NET_BIND_SERVICE ...
cap_drop: [ALL]
cap_add: [NET_BIND_SERVICE]

Narrow any SYS_ADMIN, NET_ADMIN or ALL grant

These are what turn the result into a FAIL. Remove them where the workload does not need them. Where one is genuinely required, such as NET_ADMIN for a VPN container, keep only that capability on top of --cap-drop ALL. The check will still report it, so record the exception.

Recreate the container

cap_drop and cap_add are applied when the container is created.

docker compose up -d --force-recreate

Verify the fix

docker inspect -f '{{.Name}} add={{.HostConfig.CapAdd}} drop={{.HostConfig.CapDrop}}' $(docker ps -q)
# expected: drop=[ALL] with at most a short, specific add list

# What the container's process really holds (the authoritative view):
docker exec <name> sh -c 'grep CapEff /proc/1/status'
capsh --decode=$(docker exec <name> sh -c 'grep CapEff /proc/1/status' | awk '{print $2}')
# expected: a small set, or cap_ empty for a fully dropped container

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 known weakness of this check: dropping anything satisfies it. Confirm the real capability set with the capsh --decode command above, and prefer --cap-drop ALL plus explicit adds.

VPN, firewall and network-tooling containers do need it. There is no allow-list, so this stays a finding; keep the container otherwise minimal (non-root, read-only, no socket mount) and record the exception.

Start from --cap-drop ALL, run the workload, and add back only what fails. capsh --decode on a working container shows the current set as a starting point; typical needs are CHOWN, SETUID, SETGID and NET_BIND_SERVICE.

cap_drop/cap_add are HostConfig settings applied at creation. Recreate the container (docker compose up -d --force-recreate), then re-inspect.

Capabilities are moot; the Privileged Containers check covers that and should be fixed first.

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