Post-quantum and TLS

TLS 1.3 only at the edge, the secure way

The audit wants TLS 1.2 gone. You set the minimum version, the scanner still grades you on a cipher list, and someone asks why AES-128 shows up when you only listed AES-256. Envoy is not ignoring you; it is ignoring the setting you picked.

The short answer

Set tls_minimum_protocol_version to TLSv1_3 on the Envoy listener, or tls.minVersion "1.3" in an Envoy Gateway ClientTrafficPolicy. TLS 1.2 clients then fail with a protocol_version alert. Do not use cipher_suites for TLS 1.3: Envoy applies it to TLS 1.2 and below only. If you must pin TLS 1.3 ciphers, use a compliance policy.

Updated Houssam Hammoudi, CTOTested with Envoy 1.39.1, Envoy Gateway v1.9.1 (egctl), OpenSSL 3.5.8

On this page
  1. What goes wrong
  2. What the docs say
  3. The secure configuration
  4. Prove it
  5. Mistakes people make
  6. Checklist

What goes wrong

Envoy accepts TLS 1.2 by default on every listener. So does Envoy Gateway. TLS 1.2 is not broken when configured well, but it keeps the older handshake, the older cipher negotiation and no post-quantum key exchange at all: every ML-KEM group is TLS 1.3 only. A client that can be pushed to TLS 1.2 gets classical ECDHE.

The usual fix is a minimum version. The usual mistake that follows is the cipher list. People set cipher_suites (Envoy) or ciphers (Envoy Gateway) to AES-256 only and expect TLS 1.3 to follow. It does not. Envoy's TLS library, BoringSSL, does not let you configure TLS 1.3 cipher suites, and Envoy documents the field as TLS 1.2 and below.

The last trap is the client base. Turning off TLS 1.2 locks out every client that cannot do TLS 1.3. For a public site in 2026 that is a small group, but it includes old Android devices, Java 8 without updates, and embedded clients like payment terminals or monitoring probes. Find them before they find you.

What the docs say

Minimum TLS protocol version. By default, it's TLSv1_2 for both clients and servers.

Source: Envoy docs, TlsParameters

If specified, the TLS listener will only support the specified cipher list when negotiating TLS 1.0-1.2 (this setting has no effect when negotiating TLS 1.3).

Source: Envoy docs, TlsParameters

Min specifies the minimal TLS protocol version to allow. The default is TLS 1.2 if this is not specified.

Source: Envoy Gateway API reference, ClientTLSSettings

The docs are clear about the cipher list. They do not say how to restrict TLS 1.3 ciphers instead. The answer is the compliance_policies field, covered on the CNSA 2.0 page.

The secure configuration

Envoy Gateway:

yaml
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: ClientTrafficPolicy
metadata:
  name: tls13-only
  namespace: edge
spec:
  targetRefs:
  - group: gateway.networking.k8s.io
    kind: Gateway
    name: edge
  tls:
    minVersion: "1.3"          # refuse TLS 1.0, 1.1 and 1.2
    # Add ecdhCurves here for post-quantum key exchange; see the
    # ML-KEM-1024 on Envoy Gateway page.

Envoy Gateway attaches one ClientTrafficPolicy per Gateway. If the Gateway already has one, merge these fields into it; a second policy is refused with Accepted=False (see ML-KEM-1024 on Envoy Gateway).

Standalone Envoy, the listener's TLS context:

yaml
transport_socket:
  name: envoy.transport_sockets.tls
  typed_config:
    "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
    common_tls_context:
      tls_params:
        tls_minimum_protocol_version: TLSv1_3   # refuse TLS 1.2 and older
        tls_maximum_protocol_version: TLSv1_3
        # No cipher_suites: it has no effect on TLS 1.3. To pin TLS 1.3
        # to AES-256-GCM, use compliance_policies (see the CNSA 2.0 page).
      tls_certificates:
      - certificate_chain: {filename: /etc/envoy/tls/edge.crt}
        private_key: {filename: /etc/envoy/tls/edge.key}

Before you deploy, look at your access logs for the TLS version in use. Envoy's %DOWNSTREAM_TLS_VERSION% access log operator records it per request.

Prove it

The test in secure-tests/tls-1-3-edge/run.sh runs three listener settings on Envoy 1.39.1 and probes each with OpenSSL 3.5.8: a TLS 1.3 client, a TLS 1.2 client, and a TLS 1.3 client that offers only AES-128-GCM.

bash
openssl s_client -connect edge.example.com:443 -tls1_3
openssl s_client -connect edge.example.com:443 -tls1_2
openssl s_client -connect edge.example.com:443 -tls1_3 -ciphersuites TLS_AES_128_GCM_SHA256
text
== Envoy defaults
  -tls1_3                          TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
  -tls1_2                          TLSv1.2, Cipher is ECDHE-ECDSA-CHACHA20-POLY1305
  -tls1_3 AES-128-GCM only         TLSv1.3, Cipher is TLS_AES_128_GCM_SHA256

== tls_minimum_protocol_version: TLSv1_3
  -tls1_3                          TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
  -tls1_2                          refused: alert protocol version
  -tls1_3 AES-128-GCM only         TLSv1.3, Cipher is TLS_AES_128_GCM_SHA256

== TLSv1_3 plus cipher_suites: [ECDHE-ECDSA-AES256-GCM-SHA384]
  -tls1_3                          TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
  -tls1_2                          refused: alert protocol version
  -tls1_3 AES-128-GCM only         TLSv1.3, Cipher is TLS_AES_128_GCM_SHA256

The third block is the trap: an AES-256 only cipher_suites list, and a TLS 1.3 client still gets AES-128-GCM.

The same script renders the Envoy Gateway policy offline with egctl:

text
== Envoy Gateway v1.9.1: ClientTrafficPolicy tls.minVersion "1.3", rendered offline by egctl
  ClientTrafficPolicy Accepted="True": Policy has been accepted.
                  tlsParams:
                    tlsMaximumProtocolVersion: TLSv1_3
                    tlsMinimumProtocolVersion: TLSv1_3

Against your own edge, a TLS 1.2 probe should end in alert protocol version:

bash
openssl s_client -connect www.example.com:443 -servername www.example.com -tls1_2 </dev/null 2>&1 | grep -oE "alert [a-z ]+|Protocol *: *TLSv1\.[23]"

Mistakes people make

Trusting cipher_suites to shape TLS 1.3

It only shapes TLS 1.2 and below. With a TLS 1.3 minimum, the field does nothing. Remove it so nobody reads it as a control.

Forgetting the second listener

Envoy Gateway applies a ClientTrafficPolicy to the Gateway you target. A second Gateway has its own settings, and a TLS passthrough route never touches Envoy's TLS at all: the backend decides. Scan every public IP and port, not one hostname.

Cutting off clients you did not list

Payment terminals, old Android, Java 8, and uptime monitors are the usual casualties. Check the access log field for TLS 1.2 traffic for two weeks before you switch, and tell the owners of those clients.

Leaving TLS 1.2 on the load balancer in front

If a cloud load balancer terminates TLS before Envoy, its own policy decides. Set it to TLS 1.3 as well, or pass TLS through to Envoy.

Checklist

  • Log %DOWNSTREAM_TLS_VERSION% and review TLS 1.2 traffic before the change.
  • Set tls.minVersion: "1.3" on every Gateway (or tls_minimum_protocol_version: TLSv1_3 on every Envoy listener).
  • Remove cipher_suites / ciphers from TLS 1.3 only listeners.
  • Use compliance_policies if you must pin TLS 1.3 ciphers.
  • Probe with -tls1_2 and expect alert protocol version.
  • Check load balancers and other listeners on the same IPs.

TLS 1.2 had a long career. Give it a quiet retirement, and read the access log before the party.

H2-CPQE

Learn it on a live range

Post-quantum TLS, in Edge and Post-Quantum Networking: a real host in your browser, and every objective checked on the machine.

Start free

The Dome

Want it run for you?

The Dome puts post-quantum TLS, a WAF that blocks, signed DNS and a zero-trust mesh in front of your application. Tell us what you run.

See the Dome