What this check reads
It collects the ids of every running container from docker ps -q, then runs one docker inspect across all of them with a Go template that walks each container's .Mounts and emits the container name whenever a mount's .Source is exactly /var/run/docker.sock or /run/docker.sock. Any name coming back is the FAIL and is printed in the message; an empty result is the PASS. The match is on that source path alone, so the mount's destination and its read-only flag make no difference to the outcome.
When it applies
Needs a reachable daemon and at least one running container; otherwise SKIP. It iterates each running container's .Mounts and matches a .Source of exactly /var/run/docker.sock or /run/docker.sock. Consequences of that exact match: a socket bind-mounted from a different host path, or reached through a TCP proxy or a socket-proxy container, is not detected. Read-only mounts are deliberately still reported, because the Docker API is fully usable read-only - -v /var/run/docker.sock:/var/run/docker.sock:ro grants the same host root. Stopped containers and image definitions are not evaluated.
What each result means
| Result | When | What it means |
|---|---|---|
| SKIP | Daemon unreachable / no running containers | Nothing was examined. Either no daemon answered or nothing was running to inspect, and a stopped container that mounts the socket is not reported either way. |
| FAIL | Any container mounts the socket | The named containers can drive the Docker API, which means they can create a privileged container and take the host. Read-only mounts are counted here deliberately, because they are just as exploitable. |
| PASS | None | No running container mounts the socket at either of the two standard paths. A socket reached by some other path, or through a proxy, would not have been seen. |
Why it matters
Mounting the socket gives the container full control of the daemon, which runs as root on the host: it can start a privileged container with the host root mounted. A read-only (:ro) bind mount does not help because the socket is used via connect(), not file writes. OWASP Docker Cheat Sheet Rule #1; CIS Docker 5.31 "Ensure that the Docker socket is not mounted inside any containers".
Why it fails, and when it is wrong
- Legitimate consumers: Traefik (Docker provider), Portainer, Watchtower, cAdvisor, Dozzle, some CI runners, logging agents. Each is root-equivalent if compromised.
- The check matches the source path only; a socket mounted via a symlinked path (e.g.
/docker.socksymlink) or via a socket proxy container (tecnativa/docker-socket-proxy) is not matched. The proxy pattern is the recommended mitigation and will correctly PASS only on the consumers (the proxy itself still mounts the socket and FAILs; document it). - Rootless Docker's socket lives under
/run/user/<uid>/docker.sock; mounting that is not matched (and is less dangerous, but still full control of that user's containers).
How to fix it
Find every container that mounts the socket
Include stopped containers, because the audit only sees running ones.
docker inspect -f '{{.Name}}{{range .Mounts}} {{.Source}}{{end}}' $(docker ps -aq) | grep docker.sockRemove the mount where the tool can do without it
Many tools only want the socket for convenience. Watchtower can be replaced with CI-driven deploys, and Traefik can use its file provider instead of Docker discovery. Delete the docker.sock line from the service's volumes: and recreate it.
Put a socket proxy in front of the tools that need it
For a tool that genuinely needs the API, run a filtering proxy on an internal network and allow only the API sections it uses. Do not publish the proxy's port.
services:
socket-proxy:
image: tecnativa/docker-socket-proxy
environment:
CONTAINERS: 1
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks: [socket-proxy]
networks:
socket-proxy:
internal: trueAttach the tool to the socket-proxy network and point it at tcp://socket-proxy:2375: many tools read DOCKER_HOST, and Traefik takes it as providers.docker.endpoint. The proxy itself still mounts the real socket, so it stays in this check's FAIL; record it as the accepted exception.
Recreate the containers
Mounts are fixed when a container is created.
docker compose up -d --force-recreateVerify the fix
docker inspect -f '{{.Name}}{{range .Mounts}} {{.Source}}->{{.Destination}}{{end}}' $(docker ps -q) | grep -i docker.sock
# expected: no output
# Check definitions as well as running containers:
grep -rn 'docker.sock' docker-compose.yml compose.yaml /etc/docker/compose 2>/dev/null
# Demonstrate the impact if you find one (on a test host only):
docker exec <name> sh -c 'curl -s --unix-socket /var/run/docker.sock http://localhost/version'
# any response at all means that container can create a privileged container and own the hostDebugging
Two different cases. 'Docker daemon not reachable' means the audit account could not talk to the daemon: check systemctl is-active docker, then docker ps as that user and sudo docker ps, and run the audit as root or with sudo if only the second works. 'No running containers to evaluate' means there was genuinely nothing to inspect. Neither is a pass: a stopped container that mounts the socket is never reported, so check the definitions with the grep command under Verify the fix. The Docker Inventory check covers the unreachable case in more detail.
Portainer, Watchtower, Traefik and CI agents all ask for the socket. The safe pattern is a socket proxy (tecnativa/docker-socket-proxy or similar) that exposes only the specific API endpoints the tool needs, on an internal network. The tool drops out of the finding, because it no longer mounts the real socket, but the proxy container itself still does and stays in it. Record that one as the accepted exception.
Deliberate. :ro prevents nothing: the API accepts container-creation calls over a read-only socket file, so the escape path is unchanged.
The exact-path match is the blind spot. Look for a symlinked or copied socket path, DOCKER_HOST pointing at a TCP endpoint inside the container (docker inspect -f '{{.Config.Env}}'), or a mounted /var/run directory rather than the socket file itself.
Give it the socket proxy above, or move that workload off the Docker host entirely. Do not swap the mount for an exposed TCP socket, which is strictly worse - see the Docker TCP Socket check.
Sources
- Docker: Docker daemon attack surface
- OWASP Docker Security Cheat Sheet (Rule #1)
- Quarkslab: Why is exposing the Docker socket a really bad idea?
- Tecnativa docker-socket-proxy
- Traefik: Docker provider security note (docker socket)
How the script reads this
Next
Re-run the Daemon & Socket audit after applying the fix and confirm this check moves to PASS. CtrlOps runs all 8 checks over your existing SSH connection and scores the result, so the change is visible without reading another config file.
All 8 Daemon & Socket fixes