docker ps can run privileged, mount the Docker socket with full daemon control, or expose the host network with no AppArmor profile. These misconfigurations are invisible in container lists and logs. A docker container security audit walks through runtime state with docker inspect, checks daemon settings, and compares the result against known attack patterns. This article shows the exact queries and output that reveal privilege escalation risks, the dangerous capabilities and volume mounts that matter, and how to automate the checks.
You will see real docker inspect JSON, the specific flags that fix each issue, and the docker-bench-security findings that map to CIS benchmarks. Every misconfiguration comes with a remediation command you can run immediately.
The Docker Security Model and What You Audit
Docker isolates containers with Linux namespaces (PID, network, mount, UTS, IPC, user), cgroups for resource limits, capabilities to slice up root privileges, and mandatory access control via seccomp, AppArmor or SELinux. A container without --privileged sees only its own processes, has a private network stack, and runs with a reduced capability set. The kernel enforces these boundaries, and Docker configures them at container start.
The attack surface opens when you grant --privileged, add dangerous capabilities like CAP_SYS_ADMIN, mount host paths including /var/run/docker.sock, expose ports without a firewall, disable the seccomp profile, or run as root inside the container with no user namespace remapping. docker ps shows container state, docker images shows layers, but neither reveals runtime security posture. You need docker inspect to see HostConfig.Privileged, HostConfig.CapAdd, Mounts, NetworkSettings, HostConfig.SecurityOpt and the effective user ID.
The CIS Docker Benchmark defines 100+ checks covering daemon configuration, container runtime, images and orchestrator settings. An audit starts with the runtime checks: privileged mode, capabilities, volume mounts, network exposure, security profiles and user namespaces. Then you move to daemon hardening, image scanning and orchestrator policies. For a complete walkthrough of daemon and image hardening, see Docker Security & Production Hardening.
Auditing Privileged Containers and Capabilities
Checking for Privileged Mode
A privileged container runs with all capabilities, access to all devices and no seccomp or AppArmor enforcement. It can load kernel modules, modify iptables, mount filesystems and break out to the host. Check every container:
docker inspect --format '{{.Name}} Privileged={{.HostConfig.Privileged}}' $(docker ps -q)
Output for a privileged container:
/nginx Privileged=true
The JSON shows "Privileged": true under HostConfig. If you see this in production and the container does not require kernel-level access, stop it and recreate without --privileged. Most applications never need this flag.
Enumerating Added Capabilities
Even without --privileged, a container can receive individual capabilities that enable partial escapes. List all added and dropped capabilities:
docker inspect --format '{{.Name}} CapAdd={{.HostConfig.CapAdd}} CapDrop={{.HostConfig.CapDrop}}' $(docker ps -q)
Example output:
/app CapAdd=[SYS_ADMIN NET_ADMIN] CapDrop=<nil>
CAP_SYS_ADMIN allows mounting filesystems, including /proc and /sys, which can lead to escapes. CAP_SYS_PTRACE lets you attach to host processes if PID namespaces overlap. CAP_NET_ADMIN permits network stack changes that can break isolation. For a detailed list, see capabilities(7). A container with CAP_SYS_ADMIN can mount /proc and write to /proc/sys/kernel to disable protections or trigger kernel exploits.
Fixing Capability Grants
Drop all capabilities and add only what the application needs. For a web server binding to port 80:
docker run -d --cap-drop=ALL --cap-add=NET_BIND_SERVICE nginx
Inspect the result:
docker inspect --format '{{.HostConfig.CapAdd}} {{.HostConfig.CapDrop}}' <container_id>
Output:
[NET_BIND_SERVICE] [ALL]
The container now has one capability instead of the default 14. If the application does not bind privileged ports, drop all with no additions. This reduces the kernel attack surface and limits what an attacker can do after container compromise.
Volume Mounts and Host Path Exposure
Listing All Volume Mounts
Bind mounts give a container direct access to host paths. List them:
docker inspect --format '{{range .Mounts}}{{.Type}} {{.Source}} -> {{.Destination}} ({{.Mode}}){{"\n"}}{{end}}' <container_id>
Example output:
bind /var/run/docker.sock -> /var/run/docker.sock (rw)
bind /etc -> /host-etc (rw)
volume data-vol -> /app/data ()
The first mount exposes the Docker socket with read-write access. The second mounts /etc writable. Named volumes (third line) are safer because Docker manages them and they do not map directly to arbitrary host paths.
Docker Socket and Critical Path Mounts
A container with /var/run/docker.sock mounted can control the Docker daemon. It can start a privileged container, mount the host root filesystem and execute commands on the host:
docker run --rm -v /:/host alpine chroot /host /bin/bash
This is full root access. Treat any container with the socket mounted as root-equivalent. If you must expose the socket (for CI runners or orchestrators), use a proxy like Docker socket proxy that restricts API calls, and run the container with read-only access:
docker run -d -v /var/run/docker.sock:/var/run/docker.sock:ro tecnativa/docker-socket-proxy
Other dangerous mounts: /etc (writable lets you replace passwd, shadow, sudoers), /var/lib/docker (access to all container filesystems), /root and /home. For application data, use named volumes or bind mounts with readonly:
docker run -d --mount type=bind,source=/srv/data,target=/app/data,readonly nginx
Inspect confirms "Mode": "ro". A read-only mount prevents the container from tampering with host files, even if compromised. For more on volume security and orchestrator policies, see Docker Container Security: Essential Hardening Techniques for Production.
Network Security and Exposed Ports
Enumerating Published Ports
A container can bind to all interfaces with 0.0.0.0, exposing services to the internet or local network without a firewall. Check published ports:
docker inspect --format '{{.Name}} Ports={{json .NetworkSettings.Ports}}' $(docker ps -q)
Example output for a PostgreSQL container:
/db Ports={"5432/tcp":[{"HostIp":"0.0.0.0","HostPort":"5432"}]}
This database listens on every interface. An attacker on the same network segment or with access to the host's public IP can connect directly if no firewall blocks port 5432. You also want to check .HostConfig.PortBindings, which shows the same data in a different structure. Bind services to localhost only:
docker run -d --publish 127.0.0.1:5432:5432 postgres
Inspect now shows "HostIp":"127.0.0.1". The service is accessible from the host but not from remote machines. If other containers need to reach this database, use a user-defined bridge network instead of publishing ports.
Default Bridge Network Risks
Containers on the default bridge (docker0) can see each other's traffic and perform ARP spoofing. Check which network a container uses:
docker inspect --format '{{json .NetworkSettings.Networks}}' <container_id>
Output for a container on the default bridge:
{"bridge":{"NetworkID":"...", "Gateway":"172.17.0.1", ...}}
All containers on bridge share a flat layer 2 segment. A compromised container can sniff packets or spoof MAC addresses. Create a user-defined bridge network with isolation:
docker network create --driver bridge myapp
docker run -d --network myapp --name web nginx
docker run -d --network myapp --name db postgres
Containers on myapp can resolve each other by name and communicate over an isolated network. They cannot reach containers on other user-defined networks or the default bridge without explicit connections. This limits lateral movement after a breach. For production multi-container setups, see Docker Compose & Multi-Container Applications.
Security Profiles: seccomp, AppArmor and SELinux
Checking Active Security Profiles
Docker applies a seccomp filter and an AppArmor or SELinux profile by default. These profiles block dangerous system calls and restrict file access. Check what is active:
docker inspect --format '{{json .HostConfig.SecurityOpt}}' <container_id>
Example output with default profiles:
null
A null or empty array means the defaults apply: seccomp=default and apparmor=docker-default (on systems with AppArmor) or SELinux labels (on Red Hat-based hosts). If you see:
["seccomp=unconfined"]
The container runs with no seccomp filter. It can call any system call, including those used in kernel exploits like bpf, perf_event_open and keyctl. Similarly, apparmor=unconfined or label=disable removes mandatory access control. Never disable these profiles in production. If a container was started with --security-opt seccomp=unconfined, stop it and recreate with the default profile or a custom one that permits only required syscalls.
Applying Custom seccomp Profiles
The default seccomp profile blocks about 44 system calls out of 300+. For high-security environments, create a custom profile that denies all calls except those your application uses. Docker's default profile is at moby/moby default.json. Download and edit it, or start from a deny-all template:
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": ["SCMP_ARCH_X86_64"],
"syscalls": [
{"names": ["read", "write", "open", "close", "stat", "fstat", "lseek", "mmap", "mprotect", "munmap", "brk", "rt_sigaction", "rt_sigprocmask", "ioctl", "access", "socket", "connect", "sendto", "recvfrom", "bind", "listen", "accept", "exit_group"], "action": "SCMP_ACT_ALLOW"}
]
}
Apply it with:
docker run -d --security-opt seccomp=/etc/docker/seccomp-custom.json nginx
Test the application thoroughly. If it crashes with "Operation not permitted," check dmesg for blocked syscalls and add them to the profile. Enforcement depends on kernel support; older kernels (pre-3.17) do not have seccomp. AppArmor and SELinux similarly require host kernel configuration. On Red Hat-based systems, SELinux contexts appear in SecurityOpt as label=type:svirt_lxc_net_t.
User Namespaces and Root in Container
By default, UID 0 inside a container is UID 0 on the host. If the container process escapes isolation, it has full root privileges. Check the user:
docker inspect --format '{{.Config.User}}' <container_id>
Empty output means root. Also check user namespace mode:
docker inspect --format '{{.HostConfig.UsernsMode}}' <container_id>
If empty or host, user namespaces are disabled and container root is host root. With userns-remap enabled in /etc/docker/daemon.json, the daemon maps container UIDs to a range on the host. Inside the container, the process thinks it is root, but on the host it runs as, say, UID 100000. Check /proc/self/uid_map inside the container:
docker exec <container_id> cat /proc/self/uid_map
Without remapping:
0 0 4294967295
With remapping:
0 100000 65536
The second output shows UID 0 inside maps to 100000 outside. An escape now gives you UID 100000, not root. Enable user namespaces in /etc/docker/daemon.json:
{
"userns-remap": "default"
}
Restart the daemon with systemctl restart docker. Existing volumes may have permission issues because files owned by UID 0 are now seen as UID 100000. You must chown them or recreate containers. For new containers, always set a non-root user in the Dockerfile:
USER 1000:1000
Or at runtime:
docker run -d --user 1000:1000 nginx
Inspect confirms "User": "1000:1000". This prevents root exploits inside the container from escalating to host root, even without user namespace remapping. For a complete checklist of runtime and daemon hardening, see the Docker Security Cheat Sheet.
Automating Audits with docker-bench-security
Running the CIS Docker Benchmark
The docker-bench-security script automates the CIS Docker Benchmark, checking daemon configuration, host setup, images and running containers. It runs as a container with extensive host access to inspect the Docker installation:
docker run --rm --net host --pid host --userns host --cap-add audit_control \
-v /etc:/etc:ro \
-v /usr/bin/containerd:/usr/bin/containerd:ro \
-v /usr/bin/runc:/usr/bin/runc:ro \
-v /var/lib/docker:/var/lib/docker:ro \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
docker/docker-bench-security
Output includes sections numbered 1 through 7, with each check marked [PASS], [WARN], [INFO] or [NOTE]. Sample findings:
[WARN] 5.1 - Verify AppArmor profile, if applicable
[WARN] 5.10 - Do not use the host network mode on container
[PASS] 5.15 - Do not share the host's process namespace
[WARN] 5.25 - Restrict container from acquiring additional privileges
Focus on high-impact warnings. Check 5.1 fails when containers run with apparmor=unconfined. Check 5.10 flags --net=host, which gives the container full access to the host network stack, bypassing firewall rules and exposing all listening services. Check 5.15 passes here, meaning no container uses --pid=host, which would let it see and signal host processes. Check 5.25 warns if --security-opt=no-new-privileges:true is missing, allowing privilege escalation inside the container via setuid binaries.
Interpreting and Acting on Findings
Each warning maps to a specific misconfiguration. For check 5.10, stop containers using --net=host and recreate them on a bridge network. For check 5.25, add the flag:
docker run -d --security-opt=no-new-privileges:true nginx
Inspect confirms "NoNewPrivileges": true under HostConfig. This prevents a non-root user from executing setuid binaries like sudo to gain root inside the container. Check 5.3 verifies the container platform is Linux; it always passes on Linux hosts but flags Windows containers on mixed setups. Check 5.15 looks for dangerous capabilities like CAP_SYS_ADMIN; if flagged, drop the capability as shown earlier.
For daemon-level warnings (section 2), edit /etc/docker/daemon.json and restart Docker. For image warnings (section 4), rebuild images with a USER directive and pin base image tags. Run docker-bench-security in CI/CD pipelines to gate deployments:
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock:ro docker/docker-bench-security | tee bench.log
grep -q '\[WARN\] 5.10' bench.log && exit 1
This fails the build if any container uses host networking. Adjust the grep pattern to enforce your baseline. For a Python-based audit tool that integrates with CI/CD, see Docker Container Security Audit with Python.
Daemon Configuration and Host Hardening
The Docker daemon runs as root and controls all containers. A weak daemon configuration undermines container isolation. Check /etc/docker/daemon.json for security settings. A secure baseline:
{
"icc": false,
"userns-remap": "default",
"log-driver": "json-file",
"log-opts": {"max-size": "10m", "max-file": "3"},
"live-restore": true,
"no-new-privileges": true,
"userland-proxy": false
}
"icc": false disables inter-container communication on the default bridge network, forcing you to use user-defined networks with explicit links. "userns-remap": "default" enables user namespace isolation as described earlier. "log-driver" and "log-opts" prevent log files from filling the disk; without limits, a noisy container can exhaust storage and crash the host. "live-restore": true keeps containers running during daemon upgrades, reducing downtime. "no-new-privileges": true sets the default for all containers, though you can override per-container. "userland-proxy": false disables Docker's userland proxy for port forwarding, relying on iptables instead; this reduces attack surface but requires kernel support for hairpin NAT.
Check the systemd unit for additional flags. On most distributions, /lib/systemd/system/docker.service defines ExecStart. Do not add -H tcp://0.0.0.0:2375 without TLS; this exposes the Docker API to anyone on the network, equivalent to root access. If you must expose the API, use --tlsverify with certificates:
dockerd --tlsverify --tlscacert=/etc/docker/ca.pem --tlscert=/etc/docker/server-cert.pem --tlskey=/etc/docker/server-key.pem -H tcp://0.0.0.0:2376
Clients must present a valid certificate. For details, see the Docker daemon documentation. Host-level hardening includes keeping Docker updated (subscribe to security advisories at docs.docker.com/engine/security), enabling audit logging with auditd rules for /usr/bin/docker and /var/run/docker.sock, and using a minimal host OS like CoreOS or Flatcar that reduces the attack surface. For comprehensive host hardening on Debian-based systems, see Debian Security and Hardening.
Building a Repeatable Audit Workflow
A one-time audit finds current issues, but containers change constantly. Build a repeatable workflow: run docker inspect queries for privileged mode, capabilities and mounts in a daily cron job or monitoring script. Parse the JSON with jq and alert on violations:
docker inspect $(docker ps -q) | jq -r '.[] | select(.HostConfig.Privileged==true) | .Name'
If this returns any names, you have a privileged container. Integrate docker-bench-security into CI/CD to block insecure images before deployment. Store the output in a central log aggregator and track trends: are warnings increasing or decreasing? Use the findings to update your Dockerfile templates, Compose files and orchestrator policies. For automated scanning and runtime protection, combine static analysis tools like Trivy or Grype with runtime security platforms that monitor syscalls and network activity. The Container Security Audit Cheat Sheet provides a quick reference for common queries and remediation steps.
Start with the high-impact checks: privileged containers, Docker socket mounts, host network mode and disabled security profiles. Fix these first, then move to capabilities, user namespaces and daemon settings. Every hardening step reduces the blast radius of a compromise and makes lateral movement harder. Regular audits and automated enforcement turn security from a checklist into a continuous practice.
Frequently Asked Questions
How do I check if a Docker container is running as root?
Run docker inspect --format '{{.Config.User}}' <container_id>. Empty output means the container runs as root (UID 0). You can also exec into the container with docker exec <container_id> id to see the effective UID. To fix, set USER 1000:1000 in the Dockerfile or pass --user 1000:1000 at runtime. Enabling user namespace remapping in daemon.json maps container root to a high UID on the host.
What capabilities should I drop from Docker containers?
Drop all capabilities with --cap-drop=ALL, then add only what the application needs. Most web servers only require NET_BIND_SERVICE to bind ports below 1024. Never grant CAP_SYS_ADMIN, CAP_SYS_PTRACE, CAP_SYS_MODULE or CAP_DAC_READ_SEARCH unless you understand the risk; these capabilities enable container escapes and kernel exploits. Check the application's documentation or strace its startup to identify required capabilities.
Is mounting the Docker socket in a container ever safe?
No. A container with /var/run/docker.sock mounted can control the Docker daemon, start privileged containers and execute commands on the host. This is root-equivalent access. If you must expose the socket for CI runners or management tools, use a proxy like tecnativa/docker-socket-proxy that restricts API calls, mount the socket read-only, and run the container with minimal capabilities. Treat any container with socket access as fully trusted.
How do I enable user namespaces for Docker?
Add "userns-remap": "default" to /etc/docker/daemon.json and restart the daemon with systemctl restart docker. Docker creates a dockremap user and maps container UIDs to a high range on the host (typically starting at 100000). Existing volumes may need permission fixes with chown because files owned by UID 0 are now seen as UID 100000. Check /proc/self/uid_map inside a container to confirm the mapping is active.
What does docker-bench-security check 5.10 mean?
Check 5.10 warns when a container uses --net=host, which removes network namespace isolation. The container shares the host's network stack, can see all interfaces and listening services, and bypasses firewall rules. This is dangerous because a compromised container can bind to any port and intercept traffic. Fix by removing --net=host and using a bridge network. Only use host networking when you need raw packet access or performance-critical low-latency networking.