Start the enforcer

Pair the enforcer (once)
mt-enforcer pair --api https://api.mutualtrust.io --state /var/lib/mt-enforcer # shows a pairing code; approve it in the portal (Services > your service > Pair gateway)
mt-enforcer forward-auth (caddy)
mt-enforcer forward-auth --format caddy --state /var/lib/mt-enforcer \ --listen 127.0.0.1:9191 --bundle-out /etc/mt/client-ca.pem \ --on-bundle-change "caddy,reload,--config,/etc/caddy/Caddyfile"

Caddyfile

services/enforcer/integrations/caddy/Caddyfile
# Mutual Trust with Caddy (tested with Caddy 2.11.9). # # Caddy terminates mTLS against the client CA bundle projected by mt-enforcer, # and forward_auth asks the enforcer about every request, sending the DER # certificate of this connection (overwriting any client-supplied value) with # the original method and URI. On a 2xx answer the verified identity is copied # onto the proxied request; any other answer is returned to the client. # # mt-enforcer forward-auth --format caddy --state /var/lib/mt-enforcer \ # --listen 127.0.0.1:9191 --bundle-out /etc/mt/client-ca.pem \ # --on-bundle-change "caddy,reload,--config,/etc/caddy/Caddyfile" { admin localhost:2019 auto_https off # certificates come from files below; no ACME, no HTTP redirect listener } :8443 { tls /etc/caddy/certs/server.pem /etc/caddy/certs/server-key.pem { protocols tls1.2 tls1.3 client_auth { mode require_and_verify trust_pool file /etc/mt/client-ca.pem } } # route keeps these handlers in the order written: strip, authorize, proxy. route { request_header -X-Mt-* forward_auth 127.0.0.1:9191 { uri /authz header_up X-Client-Cert-Der {http.request.tls.client.certificate_der_base64} copy_headers X-Mt-Principal X-Mt-Grant X-Mt-Request-Id } reverse_proxy origin:9001 } }

Caddy orders directives by type, not by position, so the handlers sit in a route block: inbound X-Mt-* headers are removed before forward_auth copies the verified ones from the enforcer's answer. header_up overwrites any client-supplied X-Client-Cert-Der, and forward_auth sets X-Forwarded-Method and X-Forwarded-Uri itself.

Contract

forward_auth contract
OutcomeStatusHeaders and body
allow200x-mt-principal, x-mt-grant, x-mt-request-id, x-mt-policy-revision
deny403 (returned to the client)JSON problem (type, title, code, request_id); x-mt-reason and x-mt-request-id headers

Run with Docker Compose

services/enforcer/integrations/caddy/docker-compose.yaml
# Caddy + mt-enforcer (forward_auth). # docker compose run --rm enforcer pair --api https://api.mutualtrust.io --state /var/lib/mt-enforcer # docker compose up -d services: enforcer: build: { context: ../.., dockerfile: Dockerfile } image: mt-enforcer:1.0.0 command: ["forward-auth", "--format", "caddy", "--state", "/var/lib/mt-enforcer", "--listen", "127.0.0.1:9191", "--admin", "0.0.0.0:9464", "--bundle-out", "/var/run/mt/bundle/client-ca.pem"] ports: ["8443:8443", "9464:9464"] volumes: ["mt-state:/var/lib/mt-enforcer", "mt-bundle:/var/run/mt/bundle"] healthcheck: { test: ["CMD", "/mt-enforcer", "healthcheck"], interval: 5s, retries: 30 } caddy: image: caddy:2.11.9-alpine@sha256:003a97808e781aa9f7e75cc2f43d34d461099031faf5e20faefb9f61126836f0 network_mode: "service:enforcer" depends_on: { enforcer: { condition: service_healthy } } volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - ./certs:/etc/caddy/certs:ro - mt-bundle:/etc/mt:ro origin: image: ${ORIGIN_IMAGE:-ghcr.io/your-org/orders-api:latest} volumes: { mt-state: {}, mt-bundle: {} }

Verify

Verify with the enrolled workload identity
mt enroll --token <token from portal> --api https://api.mutualtrust.io mt call https://orders-api.example.com:8443/orders/1 # 200, and your origin receives x-mt-principal: urn:mutualtrust:workload:... mt call https://orders-api.example.com:8443/admin/export # 403 {"code":"route_not_permitted", ...} # any other client: the files are in the workload directory that mt status prints W="$(dirname "$(mt status --json | jq -r .files.chain)")" curl --cert "$W/chain.pem" --key "$W/key.pem" https://orders-api.example.com:8443/orders/1

Tested

This configuration with Caddy 2.11.9 is run end to end by services/enforcer/test/real-gateways.sh: pairing, enrollment, allowed and restricted routes, a spoofed identity header, a request without a client certificate, an encoded dot-segment path, revocation inside the refresh window, and the signed acknowledgments and receipts the control plane receives.