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.

Documentation menu

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.

Sandbox quickstart
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: denied

Run in front of your API

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)
Start the proxy
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:9464

The 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 authentication
client_authWithout a client certificate
request (default)The handshake completes and every request is denied with 403 and no_client_certificate, so callers see why.
requireThe 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

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

The proxy 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.