Somebody is recording your HTTPS traffic right now. Not decrypting it. Just storing it. NGINX post-quantum key exchange is the thing that keeps that recording worthless a decade from now, and it is the subject of this article.
That is the entire threat model behind post-quantum cryptography, and it is the reason “we will migrate when quantum computers exist” is the wrong answer. A cryptographically relevant quantum computer does not need to exist today to hurt you today. It only needs to exist before your data stops mattering. Session cookies expire in hours, but the health record, the contract, the source tree, and the private key inside that TLS session stay sensitive for a decade or more. Attackers call this harvest now, decrypt later, and the only defence is to stop handing them harvestable ciphertext.
The good news is that NGINX post-quantum key exchange is not a research project. It is a one-line configuration concern on a server you already run, and if your NGINX links OpenSSL 3.5, it is very likely switched on already.
The bad news is the second half of that sentence. There is one extremely common directive that silently switches it back off, and it is sitting in thousands of hardened NGINX configurations right now, put there by advice that was correct in 2018.
This article shows you how to check which side of that line you are on, and how to fix it. Every command and configuration here was tested on Rocky Linux 10 with NGINX 1.30.4 linked against OpenSSL 3.5.8.
What NGINX post-quantum key exchange actually is
Shor’s algorithm breaks the hard problems that X25519, ECDH, and RSA rest on. It does not meaningfully break AES-256 or SHA-384. So the vulnerable part of a TLS session is not the bulk encryption. It is the key exchange at the front, where the two sides agree on the symmetric key in the first place.
NIST standardised the replacement as ML-KEM (FIPS 203, formerly Kyber), a lattice-based key encapsulation mechanism. In TLS 1.3 it is not deployed alone. Instead it is deployed as a hybrid group, and the flagship one is X25519MLKEM768.
Hybrid means the client and server run both exchanges in the same handshake and mix both shared secrets into the session key. An attacker has to break X25519 and ML-KEM-768 to recover the traffic. That design is deliberately conservative. ML-KEM is young, and if a classical break of it turns up next year, hybrid deployments degrade to exactly the security they had before. You are not betting the connection on new mathematics. You are adding it.
OpenSSL 3.5 ships four hybrid groups, all verifiable on your own box with openssl list -kem-algorithms:
| Group | Classical half | Post-quantum half |
|---|---|---|
X25519MLKEM768 |
X25519 | ML-KEM-768 |
SecP256r1MLKEM768 |
P-256 | ML-KEM-768 |
SecP384r1MLKEM1024 |
P-384 | ML-KEM-1024 |
X448MLKEM1024 |
X448 | ML-KEM-1024 |
In practice you care about the first one. Chrome and Firefox both offer X25519MLKEM768 by default today, and they offer it first. The adoption decision is therefore not yours at all. Your visitors already made it. The only question is whether your server answers.
Step one: does your NGINX have the ingredients?
Here is the misconception that wastes the most time, so deal with it before touching a configuration file.
Your distribution’s OpenSSL version is irrelevant. NGINX uses the TLS library it was linked against, which may have nothing to do with /usr/bin/openssl. A box can run OpenSSL 3.5 at the command line and still serve TLS through a library from 2023. Checking openssl version tells you about your shell, not about your web server.
Ask the binary instead:
nginx -V 2>&1 | grep "built with"
On a current GetPageSpeed build that prints:
built with OpenSSL 3.5.8+gps 25 Aug 2026
Anything below 3.5 has no ML-KEM at all, and no configuration will conjure it. That includes OpenSSL 3.0, 3.2, and 3.4, LibreSSL, and the QuicTLS 3.1 fork that most HTTP/3-capable NGINX builds were linked against until recently. If the version is too low, NGINX post-quantum support is a packaging problem rather than a tuning problem, and the fix is a different NGINX build.
Here is where things stand across the platforms we package for, verified by installing the package and reading nginx -V:
| Platform | What our NGINX links | Post-quantum today? |
|---|---|---|
| RHEL 8, 9, 10 and clones | openssl35 3.5.8 (ours) |
Yes |
| Debian 13 (trixie) | openssl35 3.5.8 (ours) |
Yes |
| Debian 12 (bookworm) | openssl35 3.5.8 (ours) |
Yes |
| Ubuntu 24.04 LTS (noble) | openssl35 3.5.8 (ours) |
Yes |
| Ubuntu 22.04 LTS (jammy) | openssl35 3.5.8 (ours) |
Yes |
| Ubuntu 20.04 LTS (focal) | openssl35 3.5.8 (ours) |
Yes |
The split is worth understanding, because it decides whether you need to do anything at all.
Since August 2026 our RPM packages link openssl35, an ABI-isolated OpenSSL 3.5 build we ship ourselves. That is the whole point of the switch away from QuicTLS, and it is what made Encrypted Client Hello possible in the same packages. On those platforms post-quantum support travels with the package and does not depend on how modern your distribution is: RHEL 8 still ships OpenSSL 1.1.1k, and our NGINX does hybrid key exchange there anyway.
Why 3.5, and why the same 3.5 everywhere
Because 3.5 is the LTS release, and that is the entire reason we chose it to standardise the fleet on.
OpenSSL designates a Long Term Support release every two years and supports it for at least five. Everything in between gets far less. The current numbers decide the argument on their own:
| OpenSSL | Status | Supported until |
|---|---|---|
| 3.5 | LTS | 2030-04-08 |
| 3.0 | LTS (expiring) | 2026-09-07 |
| 4.0 | not LTS | 2027-05-14 |
So 3.5 is not simply the newest thing that happens to have ML-KEM. It is the one release where post-quantum key exchange arrives and stays patched for another four years. Chasing the newest major number instead would buy a shorter support window than the LTS we are already on, which is why we backported Encrypted Client Hello onto 3.5 rather than moving the fleet to 4.0 to get it.
It also explains why we ship one TLS library across every platform rather than deferring to each distribution. OpenSSL 3.0 is what Ubuntu 22.04, Ubuntu 24.04 and Debian 12 ship as their system library, and its upstream support ends in September 2026, and Ubuntu 20.04’s system 1.1.1 has been past end-of-life since 2023. Your distribution vendor will keep backporting fixes to it under their own support contract, which is a real and legitimate answer. But it means your NGINX’s security posture is set by whatever each vendor chooses to carry, and it varies per platform. Linking our own LTS build makes that one decision, tracked to one date, identical on RHEL 8 and RHEL 10 alike.
Since late August 2026 our Debian and Ubuntu packages link the same openssl35 build too. Every suite we publish (Ubuntu 20.04, 22.04 and 24.04 LTS, Debian 12 and 13, on amd64 and arm64) now gets post-quantum key exchange from the package itself instead of inheriting whatever the distribution ships. That matters most on the older suites: Ubuntu 20.04’s system OpenSSL is 1.1.1, past upstream end-of-life since September 2023, and our NGINX no longer links it. On a DEB platform, nginx -V after an apt-get upgrade should report built with OpenSSL 3.5.8+gps.
Installing on RHEL, CentOS, AlmaLinux, and Rocky Linux
sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx
An existing installation upgrades in place:
sudo dnf upgrade nginx
The openssl35-libs dependency comes along automatically. It installs beside your system OpenSSL under /usr/lib64/openssl35/, with distinct SONAMEs, so it cannot disturb anything else on the machine:
ldd /usr/sbin/nginx | grep ssl
libssl.so.35.3 => /usr/lib64/openssl35/libssl.so.35.3
Installing on Debian and Ubuntu
First set up the GetPageSpeed APT repository, then install:
sudo apt-get update
sudo apt-get install nginx
On Debian and Ubuntu the package handles module loading automatically. No load_module directive is needed.
These builds link the same openssl35 as the RPMs, so post-quantum key exchange works on every suite, from Ubuntu 20.04 LTS through Debian 13. Confirm after installing or upgrading:
nginx -V 2>&1 | grep "built with"
That should report built with OpenSSL 3.5.8+gps. If it still shows the distribution’s OpenSSL, you are on a package version from before the switch: a plain apt-get upgrade brings the linked stack across, exactly as dnf does for RHEL users.
The switch brings more than post-quantum key exchange in one step: Encrypted Client Hello arrives with it (with the new nginx-ech key-rotation package), and so does a CVE stream tracked to April 2030 rather than whatever your release happens to backport. It also got our older suites off system OpenSSL versions that are already past upstream support, Ubuntu 20.04’s 1.1.1 above all, which is the part that matters even if you never touch a single group name.
Step two: check whether NGINX post-quantum is already working
Do this before you change anything, because on a default configuration the answer is usually yes. NGINX only sets an explicit key exchange group list when you tell it to. Left alone, it defers to the OpenSSL default, and OpenSSL 3.5 puts X25519MLKEM768 at the top of that default.
The most direct probe forces the client to offer nothing but the hybrid group. If the handshake completes, the server did post-quantum key exchange. If it fails, it did not. There is no ambiguity to interpret:
openssl s_client -connect www.example.com:443 -servername www.example.com \
-tls1_3 -groups X25519MLKEM768 </dev/null 2>&1 | grep -E 'New,|Negotiated'
A working server answers:
Negotiated TLS1.3 group: X25519MLKEM768
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
A server without NGINX post-quantum support answers with a handshake failure and New, (NONE), Cipher is (NONE).
That command needs a client with ML-KEM, which your system openssl may lack. On any RHEL-family box you can install ours alongside it, as a separate binary that leaves the system one untouched:
sudo dnf install openssl35
openssl35 version
Would you rather check from a browser, or check a server you have no shell on? Our SSL test runs this probe for you, along with the rest of an SSL Labs-grade audit: protocol support, cipher suites, certificate chain, HSTS, HTTP/3, and Encrypted Client Hello. It reports post-quantum key exchange as a first-class result rather than an afterthought, which most public scanners still do not.

The server-side answer, which settles arguments
Client-side probes tell you about one connection. To see what your real visitors negotiate, log it. NGINX exposes $ssl_curve (the group actually used) and $ssl_curves (the full list the client offered):
log_format pqc '$remote_addr $ssl_protocol $ssl_curve "$ssl_curves"';
log_format belongs in the http block. Attach it in the server:
access_log /var/log/nginx/pqc.log pqc;
Here are real lines from the test server, one modern client and one deliberately old one:
127.0.0.1 TLSv1.3 X25519MLKEM768 "X25519MLKEM768:SecP256r1MLKEM768:SecP384r1MLKEM1024:X25519:prime256v1:X448:secp521r1:secp384r1"
127.0.0.1 TLSv1.2 X25519 "X25519:prime256v1:X448:secp521r1:secp384r1"
That second field is the useful one for planning. It tells you what your traffic is capable of, not just what it got. Therefore you can measure how much of your audience is post-quantum-ready before you make any change at all.
Once the log has a day of traffic in it, one line gives you the NGINX post-quantum adoption rate:
awk '{ total++; if ($3 ~ /MLKEM/) pq++ }
END { printf "post-quantum: %d of %d handshakes (%.1f%%)\n", pq, total, 100*pq/total }' \
/var/log/nginx/pqc.log
On the test server, whose only clients were the probes above, that reports:
post-quantum: 5 of 8 handshakes (62.5%)
Step three: the directive that silently disables it
Now for the part that motivated this article.
For years, every serious NGINX hardening guide told you to pin your key exchange groups explicitly. The advice was sound. It looked like this:
# Do NOT copy this. It disables post-quantum key exchange.
ssl_ecdh_curve X25519:secp384r1:prime256v1;
ssl_ecdh_curve is not additive. It replaces the group list entirely. Whatever you name is what NGINX will accept, and everything you did not name is refused. In 2018 that list was exhaustive. Today it is missing the only group that matters for quantum resistance, and NGINX will enforce that omission perfectly.
Here is the same server, same binary, same OpenSSL 3.5.8, tested twice. First with the directive absent:
curve=X25519MLKEM768 proto=TLSv1.3
Then with ssl_ecdh_curve X25519:secp384r1:prime256v1; added, and nothing else changed:
curve=X25519 proto=TLSv1.3
NGINX post-quantum key exchange, gone. Meanwhile nginx -t passes and no warning is logged. The site works, the padlock appears, and the handshake quietly falls back to cryptography that a future adversary can unwind from a recording. This is the worst kind of security regression, because it is invisible from every direction except the one nobody checks.
The fix is one edit. Put the hybrid group first, and keep the classical tail so that old clients still have something to negotiate:
ssl_ecdh_curve X25519MLKEM768:X25519:secp384r1:prime256v1;
Retested immediately after:
curve=X25519MLKEM768 proto=TLSv1.3
If you have no reason to pin the list at all, the cleanest option is to delete the directive. NGINX then follows OpenSSL’s default, which already leads with the hybrid group and which will keep tracking the state of the art as the library is updated. An explicit list is a snapshot of one afternoon’s understanding, and it will age exactly the way the 2018 list aged.
The recommended NGINX post-quantum configuration
Everything below was tested end to end. It negotiates post-quantum key exchange with modern clients, serves TLS 1.3 to classical clients, and still answers TLS 1.2 clients rather than dropping them.
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name www.example.com;
ssl_certificate /etc/letsencrypt/live/www.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/www.example.com/privkey.pem;
# TLS 1.3 carries the post-quantum key exchange. Keep 1.2 so that
# clients which cannot speak 1.3 still get a page.
ssl_protocols TLSv1.2 TLSv1.3;
# Hybrid group first, classical groups after it. Anything you omit
# here is a group NGINX will refuse, so keep the classical tail.
ssl_ecdh_curve X25519MLKEM768:X25519:secp384r1:prime256v1;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}
This is the verified behaviour of exactly that block:
| Client | Result |
|---|---|
Offers X25519MLKEM768 only |
TLS 1.3, negotiated X25519MLKEM768 |
Offers prime256v1 only |
TLS 1.3, connects normally |
| TLS 1.2 only | TLS 1.2, ECDHE-RSA-AES256-GCM-SHA384 |
| HTTP/2 over ALPN | negotiates h2 |
Reload after editing, and confirm the syntax first:
sudo nginx -t && sudo systemctl reload nginx
Running the configuration through gixy, our NGINX configuration static analyser, produces no findings against this block.
If you want NGINX post-quantum to be mandatory
Some environments have a policy rather than a preference: internal APIs, machine-to-machine links, a fleet where you control both ends. There you can simply refuse anything else:
ssl_protocols TLSv1.3;
ssl_ecdh_curve X25519MLKEM768;
Tested, this behaves exactly as the name suggests. A post-quantum client connects. A classical TLS 1.3 client is rejected with a handshake failure. A TLS 1.2 client is rejected with a handshake failure.
Do not put this on a public website. You will lock out every visitor whose browser or corporate TLS-inspecting middlebox is not there yet, and they will see a connection error with no explanation. For a controlled fleet where you can verify both ends, however, it is a legitimate and strong posture.
What NGINX post-quantum key exchange costs
The reason to measure rather than assume is that ML-KEM keys are not small. An ML-KEM-768 public key serialises to 1206 bytes on this build, against 44 for an X25519 one, and the ciphertext is a comparable size. All of it travels in the handshake.
Measured on the test server, same client, same server, one group changed:
| Negotiated group | Handshake bytes read | Handshake bytes written |
|---|---|---|
X25519 |
1367 | 324 |
X25519MLKEM768 |
2441 | 1500 |
So roughly 1.1 KB extra in each direction, once, at connection setup. After the handshake the session is ordinary AES-GCM and there is no throughput difference whatsoever. The lattice arithmetic itself is fast. On any machine capable of serving TLS it disappears into the noise next to the RSA or ECDSA signature already in the same handshake.
The one number worth a second look is the 1500 bytes written. A post-quantum ClientHello no longer fits in a single Ethernet frame, so it spans multiple packets. On a healthy network that is invisible. On a path with a broken middlebox that mishandles fragmented ClientHellos, or an MTU black hole, it can surface as connections that hang rather than fail cleanly. This is rare, and it is a pre-existing defect in that path rather than something you introduced. It is worth knowing about only so you recognise the symptom if it ever appears.
Cached sessions skip the exchange entirely, which is a good argument for the ssl_session_cache line above.
HTTP/3 gets NGINX post-quantum too
QUIC uses the TLS 1.3 handshake for its own key agreement, so the same group negotiation applies. We confirmed this rather than assuming it. With listen 443 quic; configured, a QUIC client offering X25519MLKEM768 and nothing else completed the handshake and negotiated ALPN h3.
If you already run HTTP/3, the post-quantum upgrade therefore covers it with no separate work. If you do not, our packages ship it, and the HTTP/3 setup guide covers turning it on.
What about post-quantum certificates?
Key exchange is only half of TLS. Certificates are signed with RSA or ECDSA, which are equally vulnerable to Shor’s algorithm. Should you not replace those too?
Eventually, yes. Today, no, and the reason is worth being precise about, because a great deal of writing on this topic skips it.
Signature forgery is not a harvest-now-decrypt-later problem. An attacker with a quantum computer in 2035 cannot use it to retroactively forge a signature on a handshake that happened in 2026, because that handshake is over. Certificate agility matters, but the urgency is genuinely lower than for key exchange, and the deadline is the day quantum computers arrive rather than today.
That is fortunate, because the ecosystem is not ready. OpenSSL 3.5 supports ML-DSA (FIPS 204) and NGINX will serve such a certificate. We tested it, and it works:
openssl genpkey -algorithm ML-DSA-65 -out mldsa.key
openssl req -new -x509 -key mldsa.key -out mldsa.crt -days 30 -subj "/CN=internal.example.com"
Point ssl_certificate and ssl_certificate_key at those, and the handshake completes with Peer signature type: mldsa65.
However, no public certificate authority issues ML-DSA certificates, and no browser trusts them. Deploy one on a public site and every visitor gets a certificate error. Note the size too: that ML-DSA-65 certificate is 5522 bytes in DER form against 793 for the RSA-2048 certificate on the same test host, and unlike a cached key exchange it is sent on every full handshake.
The honest guidance is to use ML-DSA where you own both ends and are experimenting with your own PKI, and not to deploy it publicly. Watch this space instead of acting on it. Meanwhile, get the key exchange right, because that is the half that is exploitable retroactively and the half you can fix this afternoon.
Troubleshooting NGINX post-quantum problems
nginx -V shows an OpenSSL below 3.5. No configuration will help. Install a build linked against OpenSSL 3.5 or newer.
nginx -t fails with SSL_CTX_set1_curves_list("...") failed. A group name in ssl_ecdh_curve is unknown to your TLS library, and there are two reasons for that. Either the library predates ML-KEM (true of any OpenSSL below 3.5, though no longer of any current GetPageSpeed build on any platform), or the name is wrong. Check spelling and case: it is X25519MLKEM768, and OpenSSL does not accept the IANA form x25519_mlkem768 here. nginx -V distinguishes the two cases, and openssl list -kem-algorithms confirms what your library actually knows.
Everything looks right but $ssl_curve still shows a classical group. Something between you and NGINX is terminating TLS. A CDN, a cloud load balancer, or a reverse proxy in front of NGINX is the TLS endpoint for your visitors, and its group list is the one that counts. Test the origin directly to separate the two.
Certain clients now fail to connect. You almost certainly pinned a list without a classical fallback. Re-add the classical groups after the hybrid one.
Handshakes hang rather than fail. Suspect MTU or a middlebox that chokes on a multi-packet ClientHello, as described above. Reproduce with -groups X25519 to confirm the handshake succeeds without the larger ClientHello.
You upgraded the package but nothing changed. NGINX loads its TLS library at process start. A reload re-reads the configuration but keeps the master process, so after upgrading the library you need systemctl restart nginx rather than a reload.
Conclusion
Getting the group list right once is the easy part. Configurations drift, hardening guides get copy-pasted, and the directive that silently disabled post-quantum key exchange this year is exactly the kind of change nobody notices until an audit. GetPageSpeed Amplify runs scheduled gixy scans across every host and ties findings to live NGINX runtime metrics. Drop-in compatible with the deprecated nginx-amplify-agent (EOL January 2026).
NGINX post-quantum TLS is not a project. It is a check and, at most, a one-line edit:
- Confirm your NGINX links OpenSSL 3.5 or newer with
nginx -V. - Probe it with
openssl s_client -groups X25519MLKEM768, or run the SSL test. - If
ssl_ecdh_curveis set, putX25519MLKEM768at the front of it, or remove the directive entirely. - Log
$ssl_curveand watch adoption climb on its own, because the clients are already asking.
The interesting conclusion from doing this on real servers is that the hard part was never enabling NGINX post-quantum cryptography. It was noticing that a line of good advice from 2018 had quietly turned it off.
Our packages ship the OpenSSL 3.5 LTS build that makes this work on every platform we support, RHEL 8 upward, Ubuntu 20.04/22.04/24.04 LTS and Debian 12/13 alike, with security updates tracked to April 2030 independently of what your operating system vendor decides to backport. The Debian and Ubuntu builds joined the RPMs on that same stack in August 2026, so hybrid key exchange is now simply what apt-get install nginx gives you. Subscribe to the GetPageSpeed repository to get it, along with 100+ NGINX modules built against the same stack.