Caddy
Caddy verifies client certificates against the projected bundle and forward_auth asks mt-enforcer about every request, sending the DER certificate of the connection.
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
| Outcome | Status | Headers and body |
|---|---|---|
| allow | 200 | x-mt-principal, x-mt-grant, x-mt-request-id, x-mt-policy-revision |
| deny | 403 (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/1Tested