Back to blog

Home Lab

Putting a Guardrail on Traefik: Docker Discovery Without the Raw Socket

Traefik made service publishing pleasantly automatic. The dangerous part was how it learned what to publish: a direct line to the Docker daemon.

The first version of my container platform was intentionally simple: Docker Compose for lifecycle, Traefik for ingress, internal DNS for friendly names, and OPNsense rules to keep the service network in its lane. Applications such as Excalidraw, draw.io, Obsidian-related tooling, regex101, OneTimeSecret, and secrss could join a shared proxy network, advertise a few labels, and become reachable over HTTPS.

That workflow was excellent. The default security pattern was not.

Traefik's Docker provider needs daemon metadata so it can watch container events, read labels, and update routes. The common answer is to mount /var/run/docker.sock into the Traefik container. It works, but the Docker API is effectively a host administration interface. A compromise of the reverse proxy could become a compromise of every container and, depending on the API operations available, the host itself.

The Socket Proxy Pattern

I inserted a Docker socket proxy between Traefik and the daemon. The proxy is the only container with the Unix socket mounted. Traefik receives a TCP endpoint on a small, private Docker network and points its Docker provider at that endpoint instead.

Docker daemon socket
        β”‚
        β–Ό
socket-proxy ── private network ── Traefik
                                      β”‚
                                      β–Ό
                              application network

The proxy is not a second authorization system inside Docker. It is an HTTP filter in front of the API. That distinction matters: it reduces exposed capability, but it does not make the socket harmless.

Least Privilege for Discovery

Traefik needs only the API sections required to discover containers, networks, and events. The proxy therefore stays read-only: mutation methods remain disabled, and unrelated surfaces such as secrets, swarm administration, builds, exec, volumes, and image management remain closed. The exact allowlist should follow the provider behavior and Docker API version in use, then be verified with logs and denied-request testing.

The network boundary is equally important. I did not publish the proxy port on a host interface. Only Traefik shares the proxy network. Application containers join the separate ingress network, but they do not gain access to the daemon metadata path simply because Traefik can route to them.

This is a useful example of defense in depth: API filtering limits what Traefik can ask for, network isolation limits who can ask, and Docker's read-only socket mount prevents an accidental filesystem write to the socket path. None is sufficient alone.

Labels Are Part of the Security Boundary

Automatic discovery makes labels feel like convenience metadata, but they are configuration input to the edge proxy. I disabled exposure by default and required an explicit enable label. Each service defined its router, entry point, TLS behavior, and backend port rather than relying on broad defaults.

This prevented a newly started container from quietly becoming reachable. It also made Compose review meaningful: the same change that introduced an application showed exactly how it would be exposed.

What I Tested

TestExpected result
Start a labeled serviceTraefik discovers it and creates only the intended route.
Start an unlabeled serviceNo route is published.
Query an allowed API path from TraefikDiscovery metadata is returned.
Attempt a write or exec operationThe proxy returns a denial.
Reach the proxy from an application networkThe connection fails.

Lessons Learned

The biggest lesson was that convenience interfaces deserve the same threat modeling as public ones. Traefik was internal, but it parsed internet traffic and held a privileged view of the container environment. Reducing that view was worth a small amount of Compose complexity.

The second lesson was to separate discovery from reachability. A proxy may need to know that a container exists without every container needing a path back to the discovery API. Separate networks made that intent visible.

Finally, the socket proxy is a guardrail, not a guarantee. Pinning images, patching Traefik, constraining container privileges, protecting the dashboard, filtering east-west traffic, and maintaining recoverable configuration still matter. The best outcome was not β€œthe socket is safe.” It was β€œa compromise has fewer useful next moves.”

Further Reading

Tecnativa Docker Socket Proxy

Traefik Docker provider documentation