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 envoy-grpc
mt-enforcer envoy-grpc --state /var/lib/mt-enforcer \ --listen 127.0.0.1:9191 \ --envoy-sds-out /var/run/mt/sds/client-ca.yaml

Start Envoy after the enforcer is ready (/readyz returns 200) so the SDS file exists. The enforcer replaces it by atomic rename whenever the bundle changes, and Envoy reloads it through the watched directory.

Envoy configuration

services/enforcer/integrations/envoy/envoy.yaml
# Mutual Trust with Envoy (tested with Envoy 1.40.2). # # Envoy terminates mTLS. The client CA bundle comes from the signed policy # manifest: mt-enforcer writes it as a file-based SDS resource and swaps it by # atomic rename, which Envoy picks up through the watched directory. Every # request is then authorized by mt-enforcer over the ext_authz gRPC API with # the peer certificate attached (include_peer_certificate). The enforcer # re-validates the leaf against the same bundle and answers allow (setting # x-mt-principal, x-mt-grant and x-mt-request-id upstream, removing any other # inbound x-mt-*) or deny (403 with a JSON problem). Envoy fails closed when the # enforcer is unreachable (failure_mode_allow: false). # # Start the enforcer first so the SDS file exists: # mt-enforcer envoy-grpc --state /var/lib/mt-enforcer --listen 127.0.0.1:9191 \ # --envoy-sds-out /var/run/mt/sds/client-ca.yaml node: { id: mt-gateway, cluster: mt-gateway } # required for file-based SDS admin: address: { socket_address: { address: 127.0.0.1, port_value: 9901 } } static_resources: listeners: - name: mtls_ingress address: { socket_address: { address: 0.0.0.0, port_value: 8443 } } filter_chains: - transport_socket: name: envoy.transport_sockets.tls typed_config: "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext require_client_certificate: true common_tls_context: tls_params: { tls_minimum_protocol_version: TLSv1_2 } alpn_protocols: [h2, http/1.1] tls_certificates: # your server certificate - certificate_chain: { filename: /etc/envoy/certs/server.pem } private_key: { filename: /etc/envoy/certs/server-key.pem } validation_context_sds_secret_config: # client CA bundle from the signed manifest name: mt_client_ca sds_config: resource_api_version: V3 path_config_source: path: /var/run/mt/sds/client-ca.yaml watched_directory: { path: /var/run/mt/sds } filters: - name: envoy.filters.network.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager stat_prefix: mt_ingress forward_client_cert_details: SANITIZE # never pass a client-supplied XFCC upstream common_http_protocol_options: max_connection_duration: 300s # maximum stream age, matching the enforcer route_config: name: api virtual_hosts: - name: api domains: ["*"] routes: - match: { prefix: "/" } route: { cluster: origin, timeout: 30s } http_filters: - name: envoy.filters.http.ext_authz typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz transport_api_version: V3 failure_mode_allow: false # enforcer unreachable: deny include_peer_certificate: true # attributes.source.certificate status_on_error: { code: ServiceUnavailable } grpc_service: envoy_grpc: { cluster_name: mt_enforcer } timeout: 0.5s - name: envoy.filters.http.router typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router clusters: - name: mt_enforcer type: STATIC connect_timeout: 0.25s typed_extension_protocol_options: envoy.extensions.upstreams.http.v3.HttpProtocolOptions: "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions explicit_http_config: { http2_protocol_options: {} } load_assignment: cluster_name: mt_enforcer endpoints: - lb_endpoints: - endpoint: { address: { socket_address: { address: 127.0.0.1, port_value: 9191 } } } - name: origin # reachable only from Envoy (private network) type: STRICT_DNS connect_timeout: 1s load_assignment: cluster_name: origin endpoints: - lb_endpoints: - endpoint: { address: { socket_address: { address: origin, port_value: 9001 } } }

How the pieces fit

Envoy settings
SettingWhy
require_client_certificate: trueEnvoy proves possession of the client key and validates the chain against the SDS bundle.
validation_context_sds_secret_configThe bundle comes from the signed manifest, swapped atomically.
include_peer_certificate: trueThe leaf reaches the enforcer in attributes.source.certificate; the enforcer re-validates it and applies the policy.
failure_mode_allow: falseIf the enforcer is unreachable the request is denied.
forward_client_cert_details: SANITIZEA client-supplied x-forwarded-client-cert never reaches the origin.
max_connection_duration: 300sLong-lived connections are recycled, matching the enforcer.

Response contract

On allow the enforcer returns an OkHttpResponse that sets x-mt-principal, x-mt-grant, x-mt-request-id and x-mt-policy-revision upstream (overwriting client values) and lists every other inbound x-mt-* header in headers_to_remove. On deny it returns a DeniedHttpResponse with status 403, the JSON problem body and x-mt-reason. The gRPC health service reports SERVING only while policy is fresh.

Envoy's HTTP ext_authz filter works too: run mt-enforcer forward-auth --format xfcc with --path-prefix matching the filter's path_prefix, and set forward_client_cert_details: SANITIZE_SET with set_current_client_cert_details: { cert: true }.

Run with Docker Compose

services/enforcer/integrations/envoy/docker-compose.yaml
# Envoy + mt-enforcer (ext_authz gRPC). Envoy shares the enforcer's network # namespace, so the shipped envoy.yaml reaches it on 127.0.0.1:9191 exactly as # it would as a sidecar in a Kubernetes pod. # # docker compose run --rm enforcer pair --api https://api.mutualtrust.io --state /var/lib/mt-enforcer # docker compose up -d # # Put your server certificate in ./certs/server.pem and ./certs/server-key.pem. services: enforcer: build: { context: ../.., dockerfile: Dockerfile } image: mt-enforcer:1.0.0 command: ["envoy-grpc", "--state", "/var/lib/mt-enforcer", "--listen", "127.0.0.1:9191", "--admin", "0.0.0.0:9464", "--envoy-sds-out", "/var/run/mt/sds/client-ca.yaml"] ports: ["8443:8443", "9464:9464"] volumes: ["mt-state:/var/lib/mt-enforcer", "mt-sds:/var/run/mt/sds"] healthcheck: test: ["CMD", "/mt-enforcer", "healthcheck"] interval: 5s retries: 30 envoy: image: envoyproxy/envoy:v1.40.2@sha256:8de97e654644827d62afdbf7e1d3dc9fee01a8690d410dd1c63a30dc6b6cecbb network_mode: "service:enforcer" depends_on: { enforcer: { condition: service_healthy } } command: ["envoy", "-c", "/etc/envoy/envoy.yaml"] volumes: - ./envoy.yaml:/etc/envoy/envoy.yaml:ro - ./certs:/etc/envoy/certs:ro - mt-sds:/var/run/mt/sds:ro origin: image: ${ORIGIN_IMAGE:-ghcr.io/your-org/orders-api:latest} # your API; never published on the host volumes: { mt-state: {}, mt-sds: {} }

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 Envoy 1.40.2 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.