Implementing mTLS at the Gateway Edge
Terminating mutual TLS at the API gateway is the cornerstone of enforcing cryptographic identity before any application-layer routing or policy evaluation occurs. Without it, a compromised network segment can inject requests that appear legitimate to upstream services — no password or JWT can protect against a peer that was never required to prove its identity at the transport layer. This page covers the exact configuration sequence for Envoy 1.32+ and Kong 3.x, certificate validation mechanics, SNI-based tenant isolation, and the operational runbook for zero-downtime CA rotation.
Prerequisite Concepts
Before proceeding, make sure you understand the broader trust model this fits into. The Security Boundaries & Zero Trust section explains why network perimeter alone is insufficient and how the gateway enforces identity-aware policy. The parent API Gateway Fundamentals & Architecture topic covers the control-plane vs data-plane split that determines where certificate validation state lives. If you are choosing between Envoy, Kong, and Tyk for this workload, Kong vs Tyk vs Envoy for Microservices compares their TLS capabilities in a decision matrix.
mTLS Handshake Flow at the Gateway
The diagram below shows what happens during a mutual TLS handshake at an edge gateway: both sides exchange certificates before any HTTP byte is processed, and the gateway injects client identity into a downstream header before forwarding to the backend.
Step-by-Step Configuration: Envoy 1.32+
Listener: DownstreamTlsContext with Mandatory Client Auth
Deploy the following configuration to enforce mTLS on port 8443. The require_client_certificate: true directive causes Envoy to abort the TLS handshake if the client does not present a valid certificate — no HTTP processing begins until the transport layer is fully authenticated.
# Envoy 1.32+ — static bootstrap (production: use SDS for secret delivery)
static_resources:
listeners:
- name: edge_mtls_listener
address:
socket_address:
address: 0.0.0.0
port_value: 8443
filter_chains:
- filter_chain_match:
server_names:
- "api.corp.internal"
- "*.svc.corp.internal"
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
tls_maximum_protocol_version: TLSv1_3
cipher_suites:
- ECDHE-ECDSA-AES256-GCM-SHA384
- ECDHE-RSA-AES256-GCM-SHA384
tls_certificates:
- certificate_chain:
filename: /etc/envoy/certs/server.pem
private_key:
filename: /etc/envoy/certs/server.key
validation_context:
trusted_ca:
filename: /etc/envoy/certs/ca-bundle.pem
match_typed_subject_alt_names:
- san_type: DNS
matcher:
suffix: ".svc.corp.internal"
# Reject revoked certs via OCSP stapling
ocsp_staple_policy: STRICT_STAPLING
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: ingress_mtls
# Strip any client-supplied XFCC before injecting the validated one
forward_client_cert_details: SANITIZE_SET
set_current_client_cert_details:
subject: true
uri: true
dns: true
route_config:
name: local_route
virtual_hosts:
- name: backend
domains: ["*"]
routes:
- match: { prefix: "/" }
route: { cluster: upstream_service }
Key parameters to understand:
match_typed_subject_alt_names— enforces RFC 9525 SAN matching instead of the deprecated CN field. Usesuffixto accept any subdomain of a trusted namespace.forward_client_cert_details: SANITIZE_SET— strips any incomingX-Forwarded-Client-Certheader (preventing identity spoofing) and replaces it with the validated client identity extracted during the handshake.ocsp_staple_policy: STRICT_STAPLING— requires a valid OCSP staple from the server certificate. For client certificate revocation, add acrlpath undervalidation_context.
Enabling Session Resumption for High-Throughput Connections
Full TLS 1.3 handshakes are CPU-intensive. When clients reconnect frequently — such as services polling a control plane — configure stateless session ticket keys to allow abbreviated handshakes:
# Add inside common_tls_context on the DownstreamTlsContext
session_ticket_keys:
keys:
- filename: /etc/envoy/certs/session-ticket.key
Rotate these keys on a 24-hour schedule. They are symmetric — compromise does not expose private keys, but it allows session resumption forgery.
Step-by-Step Configuration: Kong 3.x
Kong exposes mTLS via the built-in mtls-auth plugin. Upstream traffic between Kong and backends uses a separate ssl block on the Service object.
Step 1 — Register the CA Certificate
# Kong 3.x Admin API — register the trusted CA
curl -X POST http://localhost:8001/ca_certificates \
--data-urlencode "cert@/etc/kong/certs/ca-bundle.pem"
# Returns: { "id": "ca-abc-123", ... }
Step 2 — Enable the mtls-auth Plugin on a Route
# kong.yaml (deck 1.x declarative format)
_format_version: "3.0"
ca_certificates:
- id: ca-abc-123
cert: |
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
services:
- name: backend-api
url: http://backend-service:8080
routes:
- name: protected-route
paths:
- /api/v1
protocols:
- https
plugins:
- name: mtls-auth
config:
ca_certificates:
- ca-abc-123
# Reject requests whose cert SAN does not match
skip_consumer_lookup: false
authenticated_group_by: CONSUMER_CUSTOM_ID
revocation_check_mode: STRICT
Step 3 — Inject Client Identity into Upstream Headers
Kong’s request-transformer plugin can propagate the validated client certificate’s subject as a header for backend consumption:
plugins:
- name: request-transformer
config:
add:
headers:
- "X-Client-Cert-Subject:$(client_credential.subject)"
This mirrors the XFCC injection that Envoy performs natively, giving backend services a standard identity claim without requiring them to perform their own TLS validation — a key benefit of gateway-terminated mTLS within the middleware chain before the authentication proxying layer evaluates further auth policy.
Decision Matrix: mTLS Termination Strategies
| Strategy | When to Use | Operational Trade-off |
|---|---|---|
| Terminate at gateway (recommended) | All external-to-internal traffic; services that cannot manage certificates | Simplest backend deployment; all cert lifecycle managed centrally |
| Pass-through to backend | Regulatory mandates for end-to-end encryption (e.g., PCI-DSS zone boundaries) | Backend must handle cert rotation; no XFCC pattern available |
| Re-encrypt (gateway-to-backend mTLS) | Defence-in-depth inside a service mesh; east-west encryption required | Double handshake cost; requires backend certs, but gateway still validates external client |
| SPIFFE/SPIRE SVIDs | Dynamic microservice environments; short-lived cert issuance | Eliminates long-lived cert files; requires SPIRE agent on each node |
For pure east-west traffic between services where both endpoints are already identity-aware, evaluate whether a service mesh sidecar fits better than a centralised gateway — see High Availability Topologies for a comparison of centralized ingress vs sidecar deployment models.
Gotchas and Failure Signals
SSLV3_ALERT_CERTIFICATE_UNKNOWN during handshake
The client certificate is not signed by any CA in trusted_ca. Verify the full chain is present in the bundle — intermediate CAs must be included. Confirm the SAN matcher uses the correct san_type (DNS, URI, or IP) for your certificate’s SAN field.
require_client_certificate silently ignored
This happens when a downstream load balancer or DDoS scrubber terminates TLS before the gateway. If a TCP load balancer is in front of Envoy, it must pass raw TCP through (not terminate TLS itself). Check with openssl s_client -connect <gateway-ip>:8443 -verify_return_error — if you see Verify return code: 0 without supplying a client cert, the gateway is not enforcing mTLS.
High CPU utilisation on gateway nodes
ECDHE key exchange is CPU-intensive. Monitor the envoy_listener_ssl_handshake and envoy_listener_ssl_connection_error Prometheus metrics. Horizontal scale gateway nodes before adding session ticket keys — tickets reduce load per client but do not help with net-new connections. In Kong, watch kong_datastore_reachable during CA bundle updates; a stale bundle causes all new handshakes to fail.
CA rotation causes mass connection drops
Never remove the old CA from the bundle at the moment you add the new one. Maintain a merged bundle with both CAs for the full duration of the certificate validity window of any cert signed by the old CA. In Envoy with SDS, the new bundle is pushed atomically and existing sessions continue unaffected. In Kong, update the ca_certificate object via the Admin API — it propagates within one configuration poll cycle (default 5 seconds).
XFCC header present on upstream request before gateway validation
If your backend observes X-Forwarded-Client-Cert on requests that bypassed the gateway (e.g., direct internal calls), the backend is implicitly trusting an unauthenticated claim. Add a network policy that prevents direct access to backends from anything other than the gateway’s egress IP. Never trust the XFCC header on requests that arrive on non-mTLS listeners.
match_typed_subject_alt_names rejects valid certificates
If the SAN field uses a URI type (common in SPIFFE SVIDs: spiffe://trust-domain/service-name), set san_type: URI in Envoy’s matcher. A DNS matcher will silently drop SPIFFE-issued certificates even if the CA is trusted.
Validation Checklist
-
openssl s_client -connect <gateway>:8443 -cert client.pem -key client.key -CAfile ca.pemcompletes withVerify return code: 0 (ok) - Connecting without a client certificate returns a TLS alert (handshake failure), not an HTTP 401
-
X-Forwarded-Client-Certheader is absent on requests that bypass the gateway; present and accurate on gateway-proxied requests - OCSP staple confirmed:
openssl s_client ... -statusshowsOCSP Response Status: successful - CA bundle rotation tested with the merged (old + new) bundle — no connection drops observed during rollover
- Session ticket key rotation scheduled (recommend: 24-hour rotation via cron or secrets manager)
-
envoy_listener_ssl_handshake_errorPrometheus alert configured with a threshold - SAN matcher syntax validated against a real certificate:
openssl x509 -in client.pem -noout -text | grep -A1 "Subject Alternative Name"
FAQ
Should mTLS be terminated at the gateway or passed through to backends?
Terminate at the gateway for centralized certificate lifecycle management and to offload cryptographic overhead from backend services. Pass-through only when regulatory requirements mandate end-to-end encryption to the final workload — this introduces backend certificate rotation complexity and eliminates the XFCC header injection pattern.
How do you rotate CA bundles without dropping live connections?
Use a merged CA bundle that contains both the expiring and the replacement CA during the overlap window. In Envoy, SDS (Secret Discovery Service) pushes the new bundle to all listener instances atomically without restarting the process. In Kong, update the ca_certificate object via the Admin API and the change propagates to all Workers within one config poll cycle.
What is the XFCC header and why must it be stripped at the upstream boundary?
The X-Forwarded-Client-Cert (XFCC) header carries the client certificate’s Subject, URI SAN, and fingerprint as HTTP metadata after the TLS handshake is terminated. Backends consume it to make authorization decisions. If a client can inject this header directly — bypassing TLS — they can impersonate any identity. Always strip incoming XFCC on the gateway’s listener and re-inject it only after successful certificate validation.
Parent: Security Boundaries & Zero Trust
Related
- Authentication Proxying & Token Validation — layer certificate identity into JWT claims for downstream authorization
- Implementing JWT Validation in Kong Plugins — combine mTLS client identity with JWT audience validation in the same Kong plugin chain
- Kong vs Tyk vs Envoy for Microservices — compare native TLS capabilities across gateway products
- Gateway Selection Criteria — capability matrix for choosing the right gateway for mTLS-heavy workloads