Zum Inhalt

Cloud-Native Edge Router: Traefik v3 Dynamic Discovery, Socket Proxy & Middleware Chains

Executive Summary: Maintaining static reverse-proxy configuration files across ephemeral container environments introduces manual drift and reload downtime. This architectural guide details the production deployment of Traefik Proxy v3, utilizing dynamic label-driven service discovery via a read-only Docker Socket Proxy, composable Middleware chains (rate limiting, ForwardAuth, circuit breakers, security headers), TCP/UDP SNI routing (HostSNI), and automated ACME DNS-01 certificate lifecycle management.

  • Zero-Reload Dynamic Discovery


    Discover containers, health checks, and routing rules in real time via Docker/Podman labels without reloading processes or dropping active sockets.

  • Docker Socket Isolation


    Eliminate root-equivalent /var/run/docker.sock exposure by interposing a read-only tecnativa/docker-socket-proxy restricted to CONTAINERS=1.

  • Composable Middleware Pipelines


    Chain token-bucket rate limiters, HSTS headers, IP allowlists, and OIDC/Authelia ForwardAuth middlewares across dozens of routers via single label references.

  • Unified HTTP/3, TCP & UDP SNI


    Route HTTP/3 (QUIC), raw TLS-passthrough TCP streams (PostgreSQL, Redis, SMTPS), and UDP traffic through unified EntryPoints.


1. Architecture: EntryPoints, Routers, Middlewares & Services

Traefik v3 separates static startup infrastructure (EntryPoints and Providers) from dynamic runtime routing (Routers, Middlewares, and Services):

%%{init: {'flowchart': {'wrappingWidth': 600, 'subGraphTitleMargin': {'top': 15, 'bottom': 15}}}}%%
flowchart TB
    subgraph EdgeEntry["EntryPoints (Ports 80 / 443)"]
        Web["web (:80)<br/>Permanent 308 -> websecure"]
        WebSecure["websecure (:443 TCP/UDP)<br/>TLS 1.3 + HTTP/3 + ACME"]
    end

    subgraph TraefikPlane["Traefik v3 Dynamic Pipeline"]
        Router["Router Rule Match<br/>Host(`app.nejat-hakan.de`)"]
        MWChain["Middleware Chain<br/>RateLimit -> Auth -> Headers"]
        Service["LoadBalancer Service<br/>HealthCheck + Sticky Cookie"]
    end

    subgraph ControlDiscovery["Isolated Control Plane"]
        SockProxy["Docker Socket Proxy<br/>GET /containers/json ONLY"]
    end

    subgraph BackendTier["Container Backends"]
        App1["App Replica 1<br/>172.29.0.12:8080"]
        App2["App Replica 2<br/>172.29.0.13:8080"]
    end

    Web -.->|"308 Redirect"| WebSecure
    WebSecure --> Router
    SockProxy -.->|"Watch Events"| Router
    Router --> MWChain
    MWChain --> Service
    Service --> App1
    Service --> App2

Why a Docker Socket Proxy is Mandatory

Mounting /var/run/docker.sock directly into a public-facing reverse proxy container is a critical security anti-pattern: if an attacker exploits a vulnerability in the proxy process, they can send a POST /containers/create request to the Docker API with --privileged -v /:/host, achieving instant root compromise of the physical host. By placing docker-socket-proxy on an internal socket_net network and permitting only GET requests to /containers/* and /networks/*, Traefik cannot spawn, modify, or stop containers even if compromised.


2. Production Stack: Traefik v3 + Hardened Socket Proxy

Create two distinct networks: socket_net (internal: true for isolated Docker API reads) and traefik_proxy (for routing to backend containers).

/opt/traefik/compose.yaml
services:
  socket-proxy:
    image: lscr.io/linuxserver/socket-proxy:latest
    container_name: traefik-socket-proxy
    restart: unless-stopped
    read_only: true
    tmpfs:
      - /run
    environment:
      CONTAINERS: 1
      NETWORKS: 1
      SERVICES: 1
      TASKS: 1
      POST: 0
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - socket_net

  traefik:
    image: traefik:v3.2
    container_name: traefik-edge
    restart: unless-stopped
    depends_on:
      - socket-proxy
    security_opt:
      - no-new-privileges:true
    ports:
      - "80:80/tcp"
      - "443:443/tcp"
      - "443:443/udp"
    environment:
      TZ: "Europe/Berlin"
      CF_DNS_API_TOKEN_FILE: "/run/secrets/cf_dns_token"
    volumes:
      - ./traefik.yaml:/etc/traefik/traefik.yaml:ro
      - ./dynamic:/etc/traefik/dynamic:ro
      - ./acme:/etc/traefik/acme:Z
    networks:
      - socket_net
      - traefik_proxy

networks:
  socket_net:
    internal: true
  traefik_proxy:
    external: true

3. Static & Dynamic Configuration Files

/opt/traefik/traefik.yaml
global:
  checkNewVersion: false
  sendAnonymousUsage: false

api:
  dashboard: true
  insecure: false

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
          permanent: true

  websecure:
    address: ":443"
    http3: {}
    http:
      tls:
        certResolver: letsencrypt
        domains:
          - main: "nejat-hakan.de"
            sans:
              - "*.nejat-hakan.de"
    transport:
      respondingTimeouts:
        readTimeout: 60s
        writeTimeout: 60s
        idleTimeout: 180s

providers:
  docker:
    endpoint: "tcp://socket-proxy:2375"
    exposedByDefault: false
    network: traefik_proxy
  file:
    directory: "/etc/traefik/dynamic"
    watch: true

certificatesResolvers:
  letsencrypt:
    acme:
      email: "hostmaster@nejat-hakan.de"
      storage: "/etc/traefik/acme/acme.json"
      keyType: "EC384"
      dnsChallenge:
        provider: cloudflare
        resolvers:
          - "1.1.1.1:53"
          - "9.9.9.9:53"

log:
  level: INFO
accessLog:
  format: json
  fields:
    headers:
      defaultMode: drop
      names:
        User-Agent: keep
        X-Forwarded-For: keep
/opt/traefik/dynamic/middlewares.yaml
tls:
  options:
    default:
      minVersion: VersionTLS12
      sniStrict: true
      curvePreferences:
        - X25519
        - CurveP384
      cipherSuites:
        - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
        - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
        - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
        - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256

http:
  middlewares:
    sec-headers:
      headers:
        stsSeconds: 63072000
        stsIncludeSubdomains: true
        stsPreload: true
        forceSTSHeader: true
        frameDeny: true
        contentTypeNosniff: true
        browserXssFilter: true
        referrerPolicy: "strict-origin-when-cross-origin"
        customResponseHeaders:
          Server: ""

    rate-limit-std:
      rateLimit:
        average: 100
        burst: 50
        period: 1s

    lan-whitelist:
      ipAllowList:
        sourceRange:
          - "192.168.10.0/24"
          - "10.88.0.0/24"

    # Master Composite Chain for Public Services
    public-chain:
      chain:
        middlewares:
          - rate-limit-std
          - sec-headers

    # Master Composite Chain for Internal Admin Services
    internal-chain:
      chain:
        middlewares:
          - lan-whitelist
          - sec-headers

4. Attaching Backend Containers via Traefik v3 Labels

With exposedByDefault: false and reusable file-provider middleware chains (public-chain@file, internal-chain@file), onboarding any new container requires only 4 labels:

Example Backend Service Onboarding with Traefik v3 Labels
services:
  whoami:
    image: traefik/whoami:latest
    container_name: whoami-test
    networks:
      - traefik_proxy
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.whoami.rule=Host(`whoami.nejat-hakan.de`)"
      - "traefik.http.routers.whoami.entrypoints=websecure"
      - "traefik.http.routers.whoami.middlewares=internal-chain@file"
      - "traefik.http.services.whoami.loadbalancer.server.port=80"

TCP SNI Passthrough Routing (PostgreSQL / SMTPS / TLS Backends)

Traefik v3 can route encrypted TCP streams without terminating TLS (passthrough: true) or terminate TLS at the edge and forward plain TCP:

TCP Router Rule inside /opt/traefik/dynamic/tcp-routes.yaml
tcp:
  routers:
    postgres-tls:
      entryPoints:
        - websecure
      rule: "HostSNI(`db.lan.nejat-hakan.de`)"
      service: postgres-svc
      tls:
        passthrough: false

  services:
    postgres-svc:
      loadBalancer:
        servers:
          - address: "192.168.10.50:5432"

5. Verification, ACME Storage Permissions & Diagnostics

Ensure /opt/traefik/acme/acme.json has strict 0600 permissions before startup—Traefik refuses to initialize the ACME resolver if the certificate store is group- or world-readable:

Securing acme.json & Verifying HTTP/3 + TLS 1.3 Negotiation
# 1. Enforce strict 0600 permissions on ACME certificate store
touch /opt/traefik/acme/acme.json
chmod 0600 /opt/traefik/acme/acme.json

# 2. Verify HTTP/3 (Alt-Svc h3=":443") and HSTS headers via curl
curl -ILs https://whoami.nejat-hakan.de | grep -iE "strict-transport|alt-svc|http/"

# 3. Inspect structured JSON access logs for latency > 100ms using jq
docker logs traefik-edge 2>&1 | jq 'select(.Duration > 100000000)'