
Security
Post-Quantum Cryptography (PQC) on Infrastructure You Run
Post-quantum cryptography (PQC) on infrastructure you run starts as an inventory: every TLS terminator, SSH host, CA, signing key and backup, by exposure.
Most post-quantum coverage is a standards announcement or a vendor readiness page. Neither tells the person who runs the TLS terminators, SSH bastions and private CA what to check. This article does: where public-key cryptography lives on an estate you own, how to probe what each endpoint negotiates, which defaults already protect you, and the configurations that quietly undo them.
What is post-quantum cryptography? It is public-key cryptography designed to stay secure against an attacker with a cryptographically relevant quantum computer, which would break RSA and elliptic-curve schemes. NIST published FIPS 203, defining the Module-Lattice-Based Key-Encapsulation Mechanism (ML-KEM), on August 13, 2024. In practice today, it ships as a hybrid: a classical exchange such as X25519 combined with ML-KEM, which RFC 10024 defines as providing security as long as at least one component remains secure.
Harvest now, decrypt later: why key exchange is a today problem
The threat that makes this urgent does not need a quantum computer to exist yet. NIST IR 8547, finalised in October 2026, puts it plainly: encrypted data remains at risk because of the “harvest now, decrypt later” threat, in which adversaries collect encrypted data now with the goal of decrypting it once quantum technology matures. Since sensitive data often retains its value for many years, NIST calls starting the transition now critical.
A TLS session protecting a health record or a database replication stream is only as confidential as its key exchange. Record the ciphertext today, and the session key is recoverable later from the classical ECDHE exchange — so the data's confidentiality lifetime decides whether that matters. A metrics scrape does not care; a ledger confidential for a decade does.
Signatures are different, and the difference sets your order of work. NIST IR 8547 states that signatures are not vulnerable to harvest-now-decrypt-later attacks: a forgery is useful only while the verifier still trusts the key. NIST still calls the signature transition urgent, because it takes longer and is more complex. Two lanes follow: key exchange moves now, signatures on a calendar.
The deadlines that actually exist
Post-quantum cryptography news is full of dates that turn out to be drafts or vendor guesses. These are the published ones as of November 2026 — NIST dates, EU and UK recommendations:
- NIST IR 8547 (final). RSA, ECDSA and EdDSA signatures, and finite-field and elliptic-curve Diffie-Hellman/MQV key establishment, are deprecated after 2030 and disallowed after 2035. NIST also says it will accommodate hybrid key establishment in FIPS 140 validation when combined with a NIST-approved scheme — the hybrid you deploy today is not a compliance dead end.
- US federal. Per NIST IR 8547, Executive Order 14412 and OMB Memorandum M-26-15 require the government's most sensitive systems to adopt PQC for key establishment by December 31, 2030, and for digital signatures by December 31, 2031.
- EU. The coordinated implementation roadmap (v1.1, June 2025) recommends Member States start national transition strategies by the end of 2026, move high-risk use cases no later than the end of 2030, and complete the transition for as many systems as practically feasible by 2035.
- UK. The NCSC milestones: by 2028, define goals and carry out a full discovery exercise; by 2031, carry out the early, highest-priority migration activities and refine the plan into a roadmap for completing migration; by 2035, migrate all systems, services and products.
All three agree on the first move. NIST tells organisations to treat the migration as a current operational priority, beginning with cryptographic inventories. The EU roadmap calls an inventory of cryptographic assets a first, “no-regret” step for every entity. The NCSC's 2028 milestone is literally discovery. None of them starts with an algorithm.
The inventory: where public-key crypto lives on an estate you own
On a self-hosted estate, public-key cryptography concentrates in five classes. Each has a different exposure and a different person who controls the upgrade:
- Edge TLS terminators — reverse proxies, ingress controllers, load balancers, the API gateway. Exposed to harvest-now-decrypt-later on every connection from the public internet. Usually yours to upgrade, unless the terminator is an appliance.
- SSH — host keys and key exchange on servers, bastions, CI runners and network gear. Exposed the same way: admin sessions carry credentials and configuration that outlive the session.
- Internal PKI and mesh mTLS — your private CA, the certificates it issues, service-mesh identities, the Kubernetes API server and etcd peers. Both lanes: the handshakes are a key-exchange problem, the CA hierarchy a signature problem.
- Signing keys — container image and artifact signing, package repository keys, firmware and CA roots. Not exposed to recording, but long-lived and embedded in verifiers you may not control.
- Stored ciphertext — encrypted backups, archives, exports, sealed secrets. Already harvested by definition, if a copy leaves your custody; the exposure is the data's confidentiality lifetime.
Record each asset with enough detail to decide, not just to count: which library terminates the crypto, what it negotiates today, how long its data must stay confidential, and who controls the upgrade. Keep it in version control next to the infrastructure code.
# Cryptographic inventory. Re-run the probes and update on every change.
assets:
- asset: edge-proxy-01
class: edge-tls
owner: platform-team
library: "nginx 1.28 linked against OpenSSL 3.5"
negotiated_group: X25519MLKEM768
hndl_exposure: high
confidentiality_lifetime_years: 10
upgrade_controller: us
exception_expiry: null
- asset: bastion-eu
class: ssh
owner: platform-team
library: "OpenSSH 10.2"
negotiated_kex: mlkem768x25519-sha256
hndl_exposure: high
confidentiality_lifetime_years: 5
upgrade_controller: us
exception_expiry: null
- asset: storage-array-mgmt
class: edge-tls
owner: storage-team
library: "vendor firmware (TLS stack not disclosed)"
negotiated_group: secp256r1
hndl_exposure: medium
confidentiality_lifetime_years: 3
upgrade_controller: vendor
exception_expiry: "2027-06-30"
- asset: nightly-db-backups
class: stored-ciphertext
owner: data-team
library: "age 1.2 (X25519 recipient)"
negotiated_group: null
hndl_exposure: high
confidentiality_lifetime_years: 15
upgrade_controller: us
exception_expiry: nullThe upgrade_controller field is the one that turns a list into a plan. Every row marked us is work you can schedule. Every row marked vendor is a dependency with a date on it — the subject of a later section.
Probe what you already negotiate
Do not fill the negotiated_group column from documentation. Ask the endpoint: a loop over openssl s_client gives ground truth. The probe must run from a machine with OpenSSL 3.5 or later; an older client never offers the hybrid group, so every endpoint would look classical.
#!/usr/bin/env bash
# Report which TLS 1.3 key-exchange group each endpoint negotiates.
# Requires OpenSSL >= 3.5 on the machine running the probe.
set -euo pipefail
endpoints=(
"edge.example.com:443"
"registry.internal.example.com:443"
"k8s-api.internal.example.com:6443"
)
for ep in "${endpoints[@]}"; do
host="${ep%:*}"
group=$(openssl s_client -connect "$ep" -servername "$host" \
-groups X25519MLKEM768:X25519:secp256r1 -brief </dev/null 2>&1 \
| awk -F': ' '/Negotiated TLS1.3 group|Peer Temp Key/ {print $2; exit}') || true
case "$group" in
X25519MLKEM768) status="hybrid PQ" ;;
"") status="FAILED (no handshake)" ;;
*) status="CLASSICAL ($group)" ;;
esac
printf '%-45s %s\n' "$ep" "$status"
doneThe client offers the hybrid first with two classical fallbacks, so the output shows what the server chose. A hybrid handshake prints Negotiated TLS1.3 group: X25519MLKEM768; a classical one prints a Peer Temp Key: line such as X25519, 253 bits instead, which is why the script reads both. X25519MLKEM768 is codepoint 0x11EC; its siblings SecP256r1MLKEM768 (0x11EB) and SecP384r1MLKEM1024 (0x11ED) were standardised alongside it in RFC 10024 in August 2026.
Point the same loop at the Kubernetes API server. The Kubernetes project blog reports that v1.33, built with Go 1.24 and with CurvePreferences left unset, supports X25519MLKEM768 by default; negotiation still depends on the client offering it. That is a statement about one release; later versions may change Go toolchains or settings, so probe your own apiserver rather than inferring from the version number.
SSH is simpler, because the client tells you directly. Check what the local client supports, what the server is configured to accept, and what a real connection chose:
#!/usr/bin/env bash
set -euo pipefail
# Client: is the hybrid KEX compiled in?
ssh -Q kex | grep mlkem || echo "client has no ML-KEM key exchange"
# Server (run on the host): what does sshd accept?
sudo sshd -T | grep -i '^kexalgorithms'
# Live connection: what was actually negotiated?
ssh -v -o BatchMode=yes bastion.example.com true 2>&1 | grep 'kex: algorithm' || trueWhat upgrades for free, and what silently does not
The good news is that the three stacks most self-hosted estates run on have already moved. OpenSSL 3.5 added ML-KEM, ML-DSA and SLH-DSA, changed its default supported groups to include and prefer hybrid PQC groups, and changed its default keyshares to offer X25519MLKEM768 and X25519. Go 1.24 enables X25519MLKEM768 by default when Config.CurvePreferences is nil. OpenSSH 10.0, released 2025-04-09, uses the hybrid mlkem768x25519-sha256 key agreement by default, and from OpenSSH 10.1 the client warns when a non-post-quantum key agreement is selected, telling the user the session may be vulnerable to “store now, decrypt later” attacks.
The bad news is that each default is defeated by a configuration written for a different era, and none of them fails loudly. Four patterns account for most of it.
1. A hand-written curve list in nginx
nginx's `ssl_ecdh_curve` defaults to auto, which uses the list built into the OpenSSL library. An explicit list replaces it. A hardening guide that recommended prime256v1:secp384r1 was reasonable when written; on a build linked against OpenSSL 3.5 it now removes the hybrid group, and the handshake still succeeds on a classical curve. auto only yields the hybrid when nginx is linked against OpenSSL 3.5 or later — check with nginx -V, because distribution packages often link an older library.
# excerpt: TLS settings of one server block
server {
listen 443 ssl;
server_name edge.example.com;
# Before: an explicit list that replaces OpenSSL's built-in default,
# so the hybrid group is never offered to clients.
# ssl_ecdh_curve prime256v1:secp384r1;
# After: hybrid first. Requires nginx linked against OpenSSL 3.5+.
# Keep the curve of your ECDSA certificate in the list.
ssl_ecdh_curve X25519MLKEM768:X25519:prime256v1;
}Or delete the directive and let auto decide. A list you do not maintain is a list that will be wrong again at the next transition.
2. Go version skew
Go 1.23 shipped the draft X25519Kyber768Draft00 by default; Go 1.24 removed it and replaced it with the standard X25519MLKEM768. As the Kubernetes blog points out, a Go 1.23 client talking to a Go 1.24 server has no common post-quantum group, and the handshake falls back to classical X25519. Browsers made the same codepoint move: Chrome 131 switched from the draft Kyber codepoint to standard ML-KEM. Among stale Go binaries — an operator, an exporter, a CLI in a CI image — only those built with Go 1.23 and Config.CurvePreferences left nil offer the draft group; those built with Go 1.22 or earlier offer no post-quantum group at all. Neither shares a post-quantum group with a default Go 1.24 server, so both negotiate classical X25519.
3. Istio mesh mTLS ignores your curve settings
Istio's mesh configuration reference is explicit: ecdhCurves in tlsDefaults applies only to non-ISTIO_MUTUAL traffic, and mesh mTLS does not respect ECDH curve settings. Setting curves there does nothing for workload-to-workload traffic. The lever is different: Istio 1.27 added a pqc option to COMPLIANCE_POLICY that enforces TLS 1.3 and X25519MLKEM768 on mTLS between Envoy proxies and ztunnels, Envoy's downstream and upstream TLS (gateways included), and the Istio xDS server. In ambient mode, set it in both the pilot and ztunnel containers. The release note also names two AES-GCM suites; do not read that as an AEAD restriction. Envoy's cipher_suites has no effect on TLS 1.3 and Go's `CipherSuites` covers only TLS 1.0–1.2, so TLS_CHACHA20_POLY1305_SHA256 stays negotiable on those paths. Read the source first: the policy is experimental, overrides other curve settings, and allows only X25519MLKEM768 — no X25519 fallback, unlike the OpenSSL 3.5 and Go 1.24 defaults. Any peer on those paths that does not offer the hybrid group fails the handshake. Probe both ends before the setting leaves a test namespace. On older Istio there is no equivalent switch through ecdhCurves; the upgrade is the migration step.
If your mesh identity comes from a private CA, the SPIFFE/SPIRE issuance path and the Istio authorization layer above it sit on the same handshake — fix the key exchange once, and every policy that depends on peer identity inherits it.
4. The client you forgot
A server that supports the hybrid only protects connections whose client offers it. Backup agents, database drivers, monitoring scrapers and replication links are clients too. The inventory needs both ends of every long-lived link, and the probe only tests the server half.
Failure modes: big ClientHellos and pinned appliances
The one failure that is loud has a specific cause. An ML-KEM key share is large, and tldr.fail documents the consequence: ClientHello messages that offer post-quantum cryptography exceed the threshold for transmission in a single packet, so a single TCP read() might not return the entire ClientHello. A server implementation that assumed it would breaks — the handshake fails or the connection resets the first time a PQ-capable client arrives. The same assumption can live in any device that parses the handshake in transit, so test the path, not just the endpoint.
Go documents the escape hatch: GODEBUG=tlsmlkem=0 reverts the default, which Go's release notes say can be useful when dealing with buggy TLS servers. Treat it as a tourniquet for one client, recorded in the inventory with the endpoint that needed it — not as a fleet-wide setting, which would turn off the protection everywhere to work around one bug.
Appliances are the other category: a storage array's management plane or closed firmware TLS stack may pin a classical suite you cannot change. Record it as an exception with an owner and an expiry date, raise it at renewal, and isolate what it protects. An exception with a date is a plan; one without is a permanent hole.
Stored ciphertext and backups
Stored ciphertext has no handshake to upgrade. If an archive was encrypted to a classical public key and a copy has left your custody — an offsite backup, a replicated bucket — treat it as harvested. The question is whether its confidentiality lifetime outlasts the classical encryption. A log archive kept for a year under a retention policy may not justify the work. Medical or legal records kept for decades do.
For archives you re-encrypt, age v1.3.0 added native post-quantum recipients based on HPKE with a hybrid ML-KEM-768 KEM; recipients start with age1pq1 and identities with AGE-SECRET-KEY-PQ-1. The commands below require age 1.3.0 or later:
#!/usr/bin/env bash
# Requires age >= 1.3.0 for post-quantum recipients.
set -euo pipefail
# One-time: generate a hybrid ML-KEM-768 + X25519 identity. Store it like any CA key.
age-keygen -pq -o backup-pq.key
recipient=$(age-keygen -y backup-pq.key) # prints age1pq1...
# Re-encrypt: decrypt with the old identity, encrypt to the PQ recipient.
age -d -i backup-classic.key archive-2026.tar.age \
| age -r "$recipient" -o archive-2026.pq.age
# Confirm the new file uses post-quantum encryption.
age-inspect archive-2026.pq.ageRe-encryption protects only the copies you re-encrypt. Old copies keep their exposure until destroyed, so retention-process destruction evidence belongs in the same plan.
Signatures and your own CA: rehearse, do not rush
Signatures are where owning the PKI pays off, and where patience is correct. OpenSSL 3.5 can already generate ML-DSA keys, so a private CA — the kind described in running internal PKI with step-ca and cert-manager — lets you trial post-quantum signatures on a non-production hierarchy. Issue a test intermediate and see which clients reject it. That list is the real migration scope.
#!/usr/bin/env bash
# Requires OpenSSL >= 3.5. For a test hierarchy, not production trust stores.
set -euo pipefail
openssl genpkey -algorithm ML-DSA-65 -out test-root.key
openssl req -new -x509 -key test-root.key -days 365 \
-subj "/CN=PQC Test Root" -out test-root.crt
openssl x509 -in test-root.crt -noout -text | grep 'Signature Algorithm' | head -1Do not swap a production root on enthusiasm. A root CA lives for years and is embedded in every trust store that verifies it, so plan the rotation backwards from 2030 — rehearse now, overlap the old and new roots, and retire the classical hierarchy before the disallow date rather than at it.
Exit ramps and the long game: crypto-agility as a property
The lasting lesson of this migration is not ML-KEM. It is that algorithm choice should be configuration you own, changeable without a vendor release. Three rules follow:
- Prefer library defaults to pinned lists. Every hand-written curve list in this article was correct once. Defaults move with the library; pins rot.
- Prefer hybrids to pure swaps. A hybrid stays secure if either half holds, and NIST will accommodate it in FIPS 140 validation when combined with a NIST-approved scheme. You are not betting the estate on a new primitive.
- Treat the inventory as a job, not a document. Re-run the probes on every deploy and keep the output in version control. An inventory that is written once is out of date at the next upgrade.
This is the same discipline an auditor asks for under GDPR Article 32 — measures you can demonstrate, not assert — applied to a migration with a decade of runway. Owning the TLS terminators, the CA and the backup keys means moving on your own schedule, not your vendors'. Start the inventory this quarter, because every deadline above begins with one.
§FAQ/Common questions
Frequently asked
What is post-quantum cryptography?
Post-quantum cryptography is public-key cryptography designed to resist attacks from a cryptographically relevant quantum computer, which would break RSA and elliptic-curve schemes. NIST standardised ML-KEM for key encapsulation in FIPS 203 on August 13, 2024. Today it is usually deployed as a hybrid, such as X25519MLKEM768, which combines classical X25519 with ML-KEM.
Why migrate key exchange before signatures?
Because of harvest-now-decrypt-later: traffic recorded today can be decrypted later if its key exchange is classical. NIST IR 8547 states that signatures are not vulnerable to that attack, so signature migration follows NIST's calendar — classical signatures deprecated after 2030 and disallowed after 2035.
How do I check whether my server negotiates a post-quantum key exchange?
Run openssl s_client from a machine with OpenSSL 3.5 or later, offering X25519MLKEM768 first, and read the 'Negotiated TLS1.3 group' line; a classical handshake reports a 'Peer Temp Key' line instead. For SSH, ssh -v shows the chosen algorithm in the 'kex: algorithm' line; mlkem768x25519-sha256 is the hybrid default from OpenSSH 10.0.
Why does my nginx server still negotiate a classical curve after upgrading OpenSSL?
Usually because ssl_ecdh_curve lists curves explicitly, which replaces OpenSSL's built-in default list, or because nginx is linked against an OpenSSL older than 3.5. Check nginx -V, then remove the directive or put X25519MLKEM768 first while keeping your certificate's curve in the list.
Does setting ecdhCurves in Istio enable post-quantum mTLS?
No. Istio's mesh config states that mesh mTLS does not respect ECDH curves. On Istio 1.27 and later, the experimental COMPLIANCE_POLICY=pqc option enforces TLS 1.3 with X25519MLKEM768 on mesh mTLS, gateways and xDS and allows only that group, so a peer that does not offer it fails the handshake; test it in one namespace first. In ambient mode it must be set in both the pilot and ztunnel containers.
Further reading
Executive Briefing
Thirty minutes to clarify your infrastructure risk
Walk us through your vendor footprint and regulatory constraints. We will tell you honestly where sovereignty creates leverage — and where it does not. No pitch deck. No obligation.