Standalone mTLS proxy
In proxy mode the enforcer is the gateway: it terminates TLS with your server certificate, requests a client certificate, validates it against the signed bundle, authorizes every request and forwards allowed requests to your origin.
Quickstart
mt gateway starts a sample Orders API and this proxy in front of it, pairs it, and prints the next steps. It uses a development certificate for localhost.
mt gateway --api https://api.mutualtrust.io
# in another terminal
mt enroll --token <token from portal>
mt call https://localhost:8443/orders
mt call https://localhost:8443/admin/export # restricted route: deniedRun in front of your API
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 proxy --state /var/lib/mt-enforcer \
--listen 0.0.0.0:8443 \
--server-cert /etc/mt-enforcer/tls/server.pem --server-key /etc/mt-enforcer/tls/server-key.pem \
--origin http://127.0.0.1:9001 \
--admin 127.0.0.1:9464The server certificate and key are re-read when the files change, so certificate renewal needs no restart.
What reaches the origin
Every inbound x-mt-* header and every forwarded-certificate header (x-forwarded-client-cert, x-ssl-client-cert, x-amzn-mtls-* and others) is removed. Allowed requests carry x-mt-principal, x-mt-grant and x-mt-request-id, plus any static proxy.origin.headers you configure (for example a shared secret the origin checks). Responses carry x-mt-request-id and x-mt-policy-revision. For an HTTPS or mutually authenticated origin set proxy.origin.ca_file, cert_file and key_file.
Client certificates
| client_auth | Without a client certificate |
|---|---|
request (default) | The handshake completes and every request is denied with 403 and no_client_certificate, so callers see why. |
require | The TLS handshake fails. |
The CertificateRequest advertises the CAs of the current bundle. Proof of possession is checked by the TLS handshake; the chain is validated by the enforcer against the signed bundle on every request, including requests on keep-alive and resumed sessions.
Long-lived connections
Connections older than max_stream_age (default 5 minutes) are closed. When a new policy revision removes every grant of a principal, that principal's open connections are closed at once; when policy goes stale, connections are closed and new requests are denied.
Behind a load balancer
When a load balancer terminates mTLS and forwards the certificate, run the proxy with --peer-source forwarded, the header --format (aws-alb, cloudflare, xfcc or rfc9440) and --trusted-proxies set to the load balancer's source addresses. Requests from any other address are refused with untrusted_forwarder before the header is read. See AWS ALB.
Verify
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