The Docker Socket Is Root
Half the self-hosted monitoring tools in existence want the Docker socket. So do reverse proxies that discover backends by label, backup tools that enumerate volumes, and dashboards that show you what is running.
The line is always the same:
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
That line gives the container root on the host.
Why#
The Docker daemon runs as root, and its API is not a restricted control
surface – it is a way to ask root to do things. A process that can talk to the
socket can start a container with the host filesystem bind-mounted at /host
and --privileged set, then write whatever it likes wherever it likes. There is
no privilege escalation step to find; the API is the escalation.
The :ro does nothing useful here#
This is the part that surprises people, and it is worth being precise about.
:ro marks the mount read-only, which stops the container changing
permissions on the socket file. But a Unix socket is not a file you read and
write in the ordinary sense – it is an endpoint you connect to and exchange
messages over. Marking the mount read-only does not make the socket
stop accepting arbitrary requests.
So the flag is not wrong, exactly. It is answering a different question from the one everybody is asking it.
What actually helps#
Put a filter in front of the socket. A socket proxy sits between the container and the daemon and refuses API calls the container has no business making, usually configured as a set of allowed endpoint groups.
The common one is Tecnativa/docker-socket-proxy, which is configured by environment variable, defaults to denying everything, and is turned on one endpoint group at a time:
services:
socket-proxy:
image: tecnativa/docker-socket-proxy
environment:
CONTAINERS: 1 # list and inspect containers
POST: 0 # no state-changing calls at all
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks: [socket]
dashboard:
image: some/dashboard
environment:
DOCKER_HOST: tcp://socket-proxy:2375
networks: [socket]
POST: 0 is doing most of the work. Nearly everything dangerous in the Docker
API is a POST – creating containers, starting them, executing into them – and
the tools that merely want to see what is running need none of them.
The proxy still has the socket, so it is still the thing to keep small and patched. The gain is that one container has root instead of five, and that one container runs a small piece of software whose entire job is to say no.
The question to ask each container#
Not "does this need the socket" but "which four API calls does this need." Most monitoring tools need to list containers and read their stats. That is a read-only subset, and it is the difference between a compromised dashboard leaking a container list and a compromised dashboard owning the host.
Treat that question as an inventory item, not a one-time compose-file review. Whenever a container gains socket access, record the endpoints it needs, put it behind the narrowest proxy policy that works, and revisit the policy when the container is upgraded. A socket mount is infrastructure-level access; it deserves infrastructure-level change control.
Sources#
- Tecnativa, docker-socket-proxy
- 11notes, docker-socket-proxy: read-only, rootless, distroless
