Protect the Docker socket
# The socket must not be reachable by anyone you wouldn't give root tosudo chown root:docker /var/run/docker.socksudo chmod 660 /var/run/docker.sock
# Audit who is in the docker group — this list is your root user listgetent group dockerAnd in every compose file and docker run you own: remove any bind mount of
/var/run/docker.sock.
Why it matters
Section titled “Why it matters”The Docker daemon runs as root and exposes an API that creates containers with arbitrary mounts and arbitrary privileges. That means anything which can reach the socket can run this:
docker run -v /:/host --privileged -it alpine chroot /host shThat is a root shell on the host, in one command, using nothing but documented API behaviour. There is no exploit here and no vulnerability to patch — the API is doing exactly what it’s for.
Everything follows from that:
- The
dockergroup is root. Adding someone to it is granting root, and Docker’s documentation says so. Treatgetent group dockeras the list of people with root on the box. - Mounting the socket into a container gives that container root on the host. Not root in the container — root on the host.
- Exposing the daemon over TCP publishes a root API to the network.
The socket mount is everywhere, and it’s the same thing every time
Section titled “The socket mount is everywhere, and it’s the same thing every time”This line appears in an enormous number of compose files:
volumes: - /var/run/docker.sock:/var/run/docker.sock # this is root on the hostIt’s how Portainer manages containers, how Traefik discovers them, how Watchtower updates them, and how a great many CI runners build images. Each of those has a real reason for it. The reason doesn’t change the consequence: a compromise of that container is a compromise of the host, and containers running web UIs are not a small attack surface.
If a container genuinely needs the API, put a filtering proxy in front of it rather than handing over the raw socket:
services: dockerproxy: image: tecnativa/docker-socket-proxy environment: CONTAINERS: 1 # allow read-only container listing POST: 0 # deny everything that changes state volumes: - /var/run/docker.sock:/var/run/docker.sock traefik: environment: DOCKER_HOST: tcp://dockerproxy:2375 # no raw socket hereThe proxy holds the dangerous mount, exposes only the endpoints you allow, and is a much smaller thing to review than Traefik.
Never expose the daemon over TCP
Section titled “Never expose the daemon over TCP”dockerd -H tcp://0.0.0.0:2375 # an unauthenticated root API, on the networkPort 2375 is plaintext and unauthenticated. Port 2376 is TLS, and TLS alone still isn’t authentication unless you also require client certificates. Internet scanners look for both continuously, and an open 2375 is found in minutes.
If you truly need remote Docker, the right answer in 2026 is not TCP at all — it’s SSH:
export DOCKER_HOST=ssh://user@remote-hostThat reuses SSH’s authentication, needs no extra port, no
certificate infrastructure, and no daemon flag. It is strictly better than
--tlsverify, and it is why exposing 2375/2376 has no remaining justification.
Related
Section titled “Related”- Rootless mode — the structural fix for all of this.
- Container escape — what socket access actually buys an attacker.