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.

mTLS Handshake Sequence at the API Gateway Edge Sequence diagram showing the mutual TLS handshake between client, gateway, and backend service, followed by XFCC header injection and forwarding. Client API Gateway Backend Service 1. ClientHello + SNI 2. ServerHello + server cert + CertificateRequest 3. Client cert + CertificateVerify + Finished 4. Validate client cert CA bundle · SAN · OCSP/CRL 5. HTTP + XFCC header injected 6. Backend response 7. Response to client TLS terminated at gateway Backend receives plaintext or re-encrypted traffic

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. Use suffix to accept any subdomain of a trusted namespace.
  • forward_client_cert_details: SANITIZE_SET — strips any incoming X-Forwarded-Client-Cert header (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 a crl path under validation_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.pem completes with Verify return code: 0 (ok)
  • Connecting without a client certificate returns a TLS alert (handshake failure), not an HTTP 401
  • X-Forwarded-Client-Cert header is absent on requests that bypass the gateway; present and accurate on gateway-proxied requests
  • OCSP staple confirmed: openssl s_client ... -status shows OCSP 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_error Prometheus 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