Skip to content

Protect the Docker socket

Severity: criticalApplies to: Docker Engine 27.x+Applies to: Docker Engine 29.x
The fix
Terminal window
# The socket must not be reachable by anyone you wouldn't give root to
sudo chown root:docker /var/run/docker.sock
sudo chmod 660 /var/run/docker.sock
# Audit who is in the docker group — this list is your root user list
getent group docker

And in every compose file and docker run you own: remove any bind mount of /var/run/docker.sock.

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:

Terminal window
docker run -v /:/host --privileged -it alpine chroot /host sh

That 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 docker group is root. Adding someone to it is granting root, and Docker’s documentation says so. Treat getent group docker as 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 host

It’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 here

The proxy holds the dangerous mount, exposes only the endpoints you allow, and is a much smaller thing to review than Traefik.

Terminal window
dockerd -H tcp://0.0.0.0:2375 # an unauthenticated root API, on the network

Port 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:

Terminal window
export DOCKER_HOST=ssh://user@remote-host

That 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.