Don't Give Your Reverse Proxy Root on the Host
Traefik needs to know which containers exist and what labels they carry — it does not need exec, write, or image management. tecnativa/docker-socket-proxy is the difference between those two things.
The most common Traefik tutorial online mounts /var/run/docker.sock:/var/run/docker.sock straight into the Traefik container. On vmi2633427 that line does not exist anywhere in the compose file, and it never will.
One path gives an attacker the host. The other gives them a container list.
What the docker provider actually needs
Traefik's Docker provider watches the Docker API for one purpose: discover running containers, read their labels, and build routers from them. That is a read operation over a small slice of the API — list containers, inspect labels. It has no legitimate reason to create containers, execute commands inside them, pull images, or touch volumes.
The Docker socket does not offer that narrow a permission model. Anything with access to /var/run/docker.sock can do everything the Docker CLI can do, because the socket is the Docker API, unscoped. That includes spinning up a new container with -v /:/host and reading the entire host filesystem — a well-known escape from "container access" to "root on the box."
The proxy that only answers one question
docker-compose.yml on vmi2633427 runs tecnativa/docker-socket-proxy as an intermediary. It is the only container that mounts the real socket. Traefik talks to it over the network instead:
tcp://dockerproxy:2375The proxy is configured with CONTAINERS=1 and nothing else enabled — every other API group (EXEC, POST, IMAGES, NETWORKS, VOLUMES, SERVICES, SWARM, and so on) defaults to off. Traefik can list containers and read their metadata. It cannot start, stop, or exec into anything, and it cannot ask Docker to build or pull an image.
Where the proxy actually lives
This scoping is reinforced by network placement, not just environment variables. dockerproxy sits on the socket bridge — 172.18.1.0/24, internal, no NAT — alongside Traefik and nothing else. Even a container that somehow found itself on frontend (the network Traefik's docker provider watches for routable services) has no route to dockerproxy; it is a separate, isolated network. The socket proxy is reachable from exactly one client.
The blast radius, stated plainly
If Traefik itself is ever compromised — a bad middleware plugin, a bug in a version upgrade, a misconfigured router exposing something it shouldn't — the difference between these two designs is total:
- Raw socket mounted: the attacker has the Docker API, unscoped. Game over for the host.
- Socket proxy,
CONTAINERS=1: the attacker can enumerate container names and labels. That is roughly equivalent to whatdocker psalready tells anyone who can already reach the box's services — not nothing, but not a host takeover either.
The pattern generalizes
This is the general shape of least-privilege delegation for any component that needs some visibility into infrastructure but not control of it: put a narrow, purpose-built broker in front of the powerful interface, and give the broker itself no more network reachability than the one client it serves. The same logic applies to giving a CI runner scoped cloud credentials instead of an admin key, or giving a monitoring agent read-only Kubernetes RBAC instead of cluster-admin. The question worth asking before mounting any socket, any credential, or any shared filesystem into a container is not "does this make it work" — plain docker.sock access makes everything work — but "what is the smallest interface that makes it work."
Series: The Self-Hosting Stack. Next: the three Docker networks that keep firewall rules from moving under you.