In a Kubernetes cluster, hybrid post-quantum TLS is configured wherever the handshake actually ends, almost never in the application container. That point is usually the ingress controller pod, a cloud load balancer in front of it, or a service mesh sidecar. The work has four steps: find the real termination point, check the TLS library version running there, put a hybrid group such as X25519MLKEM768 ahead of a classical group in the preference list, then verify from outside the cluster.
Step 1: find where the handshake really ends
Teams often configure the wrong component and conclude that their controller does not support post-quantum key exchange. Map the path of a client packet first. Only the component that owns the private key and sends the ServerHello decides the key exchange group.
| Termination point | Who chooses the key exchange group | What to check first |
|---|---|---|
| Ingress controller pod (nginx, Envoy based, HAProxy, Traefik) | The controller, through its ConfigMap or its own configuration object | The TLS library version inside the controller image |
| Cloud layer 7 load balancer terminating TLS | The provider, through a named TLS or security policy | Whether the provider offers a policy that includes a hybrid group |
| Cloud layer 4 load balancer in passthrough or TCP mode | The ingress controller behind it, because the handshake travels through | That the service really is TCP passthrough, not TLS terminating |
| CDN, WAF or reverse proxy in front of the cluster | That edge service, for every public hostname it fronts | Its documented TLS configuration options per hostname |
| Service mesh sidecar (east to west mutual TLS) | The mesh control plane and the sidecar proxy build | Whether internal traffic is in scope for this phase at all |
Confirm the topology with read-only commands before changing anything:
kubectl get ingress -A -o wide
kubectl -n ingress-nginx get svc -o wide
kubectl -n ingress-nginx get svc ingress-nginx-controller -o jsonpath='{.metadata.annotations}'
Load balancer annotations on the controller Service tell you whether the cloud layer terminates TLS or passes it through. If it terminates, no ingress setting will change the negotiated group.
Step 2: check what the controller's TLS library supports
The hybrid groups are a property of the TLS stack compiled into the controller image, not of Kubernetes. For OpenSSL based builds, OpenSSL 3.5 or later is required for the ML-KEM (Module-Lattice-Based Key-Encapsulation Mechanism) groups. BoringSSL based data planes such as Envoy use their own group naming and their own support matrix, so read the release notes for the exact version you run.
kubectl -n ingress-nginx exec deploy/ingress-nginx-controller -- openssl version
If that version is older than 3.5, stop here and plan a controller upgrade. No configuration key can enable a group the library does not implement.
The group names and IANA codepoints you may legitimately use are published in the TLS parameters registry (iana.org):
X25519MLKEM768(0x11EC, 4588), the de facto default for browsers today, described on our X25519MLKEM768 page.SecP256r1MLKEM768(0x11EB, 4587) andSecP384r1MLKEM1024(0x11ED, 4589), for environments that require NIST curves.- Pure ML-KEM groups
MLKEM512,MLKEM768andMLKEM1024(512, 513, 514). These are not hybrid: most readers should not deploy them at the public edge yet.
Hybrid key agreement for TLS 1.3 is specified in RFC 10024 (rfc-editor.org). ML-KEM itself is FIPS 203, final since 13 August 2024 (csrc.nist.gov). The pre-standard Kyber draft group X25519Kyber768Draft00 (0x6399) is obsolete: if it appears anywhere in your configuration, remove it.
Step 3: set the groups, with a classical fallback
Only one key exchange group is negotiated per connection, and the server picks from what the client offered. Listing a hybrid group first and a classical group after it is therefore safe by construction: clients that know the hybrid group use it, older clients fall back to X25519 or P-256.
ingress-nginx
The controller exposes the ConfigMap key ssl-ecdh-curve, which maps to the nginx directive ssl_ecdh_curve and is passed to the TLS library as an ordered group list (its default value is auto). Confirm the key in the ConfigMap reference for the controller version you run, then apply:
apiVersion: v1
kind: ConfigMap
metadata:
name: ingress-nginx-controller
namespace: ingress-nginx
data:
ssl-protocols: "TLSv1.3 TLSv1.2"
ssl-ecdh-curve: "X25519MLKEM768:X25519:prime256v1"
Here prime256v1 is the OpenSSL name for secp256r1. Keeping two classical groups after the hybrid one preserves TLS 1.2 clients and any TLS 1.3 client that does not implement ML-KEM. The same ordering logic applies to a standalone web server, as described in our guide to enabling ML-KEM on nginx, Apache, HAProxy and Caddy.
Other controllers and service meshes
Do not copy annotations between controllers. An annotation the controller does not recognise is silently ignored, which is the most dangerous failure mode here: the rollout looks successful and nothing changed. The principle is identical everywhere, the surface differs:
- Envoy based data planes expose TLS parameters that include an ECDH curves list. In a mesh, that field is usually reached through an advanced patch mechanism rather than a first class API, so check the documentation for your mesh and proxy version.
- HAProxy and Traefik based controllers set the group list in their own configuration objects. Look up the exact key in the controller documentation for your version, and validate it in staging.
- If a managed load balancer terminates TLS, your only lever is the provider's TLS policy. Where no hybrid policy is offered, the alternative is to move termination into the cluster behind a layer 4 passthrough service, which is an architectural change and deserves its own review.
Leave the certificate chain alone for now
Recorded traffic is protected only by the key exchange, which is the whole point of harvest now, decrypt later: an attacker who stores ciphertext today can decrypt it later if the key exchange was classical. Our article on harvest now, decrypt later explains why key exchange is first in the queue. Certificates can migrate later: public certificate authorities still issue ECDSA and RSA certificates, and ML-DSA signatures, standardized as FIPS 204, are not yet generally available in public TLS chains. Keep your existing cert-manager issuer unchanged in this phase.
Step 4: verify from outside the cluster
Test from a machine on the public internet, through the same path a browser takes. Never verify with kubectl port-forward: it bypasses the load balancer and the edge, so it can show a hybrid handshake that no real client will ever get. With OpenSSL 3.5 or later:
openssl s_client -connect www.example.com:443 -servername www.example.com \
-groups X25519MLKEM768 </dev/null 2>/dev/null | grep -i 'Negotiated TLS1.3 group'
A successful result prints the negotiated group. Then prove the fallback still works, which is the test teams forget:
openssl s_client -connect www.example.com:443 -servername www.example.com \
-groups X25519 </dev/null 2>/dev/null | grep -i 'Negotiated TLS1.3 group'
Repeat both tests for every public hostname, including the apex and any host served by a second ingress class or a different edge provider. A cluster with two ingress classes commonly ends up half migrated. What an external test can and cannot show is described in our scanner methodology.
Step 5: roll out, and keep the evidence
- Apply the change in staging first, on a controller running the same image tag as production.
- Promote to one production hostname or one canary ingress class, not the whole fleet.
- Watch TLS handshake errors and controller CPU for at least one full business cycle. ML-KEM key shares are larger than an X25519 share, so ClientHello and ServerHello messages grow.
- Keep the rollback ready: reverting the ConfigMap key to its previous value restores the old behaviour on the next reload.
- Record the result in your cryptographic inventory, with the date and the host tested.
That last step is where a platform change becomes compliance evidence. The mapping below is our reading of how the technical artifact serves the requirement, not a quotation of the texts. This is information, not legal advice.
| Requirement | What the text asks for | Artifact from this change |
|---|---|---|
| PCI DSS v4.0.1 req. 12.3.3 | Documented inventory of cipher suites and protocols in use, reviewed at least every 12 months | The controller ConfigMap, under version control, plus the dated external test output |
| DORA RTS (EU) 2024/1774, Art. 6 | An encryption policy with provisions for changing cryptographic technology as cryptanalysis evolves | The documented group ordering and the tested rollback procedure |
| DORA RTS (EU) 2024/1774, Art. 7 | Key management over the lifecycle and a register of certificates for critical assets | The cert-manager issuer inventory for ingress hostnames |
Next step
Once the ConfigMap is applied and your staging tests pass, confirm the externally visible result: scan your domain with PQC Status to see which key exchange group your ingress actually negotiates, which TLS versions it offers, and what the certificate chain looks like. Run the scan once per public hostname, before and after each controller image upgrade, because an image change can silently move you back to a classical group. A scan describes the externally visible TLS configuration only: it is not a certification and not a compliance attestation.