yum upgrades for production use, this is the repository for you.
Active subscription is required.
Technical Briefing: NGINX Post-Quantum TLS — X25519MLKEM768 with OpenSSL 3.5
The Problem: Harvest Now, Decrypt Later
The threat model is not a quantum computer breaking TLS today — it is an attacker recording encrypted traffic now and decrypting it once a cryptographically relevant quantum computer exists. Session cookies expire in hours, but health records, contracts, source trees, and private keys remain sensitive for a decade or more.
The precise vulnerability matters: Shor’s algorithm breaks X25519/ECDH/RSA key exchange, not AES-256 or SHA-384. Bulk encryption survives; the key agreement step does not.
The Fix: Hybrid Key Exchange in TLS 1.3
NIST-standardized ML-KEM (FIPS 203, formerly Kyber) is deployed in TLS 1.3 as the hybrid group X25519MLKEM768. Both exchanges run in the same handshake and both shared secrets are mixed into the session key, so an attacker must break both. If ML-KEM is broken classically, hybrid deployments degrade to prior security rather than failing outright.
OpenSSL 3.5 ships four hybrid groups, verifiable with:
openssl list -kem-algorithms
Chrome and Firefox already offer X25519MLKEM768 by default and offer it first — the adoption decision belongs to visitors, not server operators.
Critical Check: The Distribution’s OpenSSL Version Is Irrelevant
NGINX uses the TLS library it was linked against, not the system library. Check with:
nginx -V 2>&1 | grep "built with"
Anything below OpenSSL 3.5 has no ML-KEM at all — including 3.0, 3.2, 3.4, LibreSSL, and the QuicTLS 3.1 fork most HTTP/3-capable NGINX builds used until recently. Post-quantum support is a packaging problem, not a tuning problem.
Packaging: ABI-Isolated OpenSSL 3.5
Since August 2026, RPM packages link openssl35, an ABI-isolated OpenSSL 3.5 build shipped alongside the system library under /usr/lib64/openssl35/ with distinct SONAMEs. Debian and Ubuntu packages joined the same stack in late August 2026, covering Ubuntu 20.04/22.04/24.04 LTS and Debian 12/13 on amd64 and arm64.
This means RHEL 8 (system OpenSSL 1.1.1k) and Ubuntu 20.04 (1.1.1, past upstream EOL since 2023) get hybrid key exchange anyway. OpenSSL 3.5 is the LTS release — supported at least five years, with security updates tracked to April 2030 — which is why Encrypted Client Hello was backported onto 3.5 rather than moving the fleet to 4.0.
Verification
Force the client to offer only the hybrid group:
openssl s_client -connect host:443 -servername host -tls1_3 -groups X25519MLKEM768
- Success prints
Negotiated TLS1.3 group: X25519MLKEM768 - Failure prints
New, (NONE), Cipher is (NONE)
A client with ML-KEM is required — on RHEL-family systems, install openssl35 as a separate binary that leaves the system one untouched.
Logging and Measurement
NGINX exposes two variables:
$ssl_curve— the group actually used$ssl_curves— the full list the client offered
Define a log_format in the http block and attach with access_log in the server block. $ssl_curves reveals what traffic is capable of, not just what it negotiated, so post-quantum readiness can be measured before any change. A one-line awk over the log yields the adoption rate.
The Silent Regression
ssl_ecdh_curve is not additive — it replaces the group list entirely. The 2018-era hardening advice:
ssl_ecdh_curve X25519:secp384r1:prime256v1;
is exhaustive for its time but omits the only quantum-resistant group, and NGINX enforces that omission perfectly. Same server, same OpenSSL 3.5.8:
- Directive absent →
curve=X25519MLKEM768 - Directive added →
curve=X25519
nginx -t passes, no warning is logged, the padlock appears, and the handshake quietly falls back.
The One-Line Fix
ssl_ecdh_curve X25519MLKEM768:X25519:secp384r1:prime256v1;
Hybrid group first, classical tail retained for old clients. If there is no reason to pin the list at all, deleting the directive is cleanest: NGINX follows OpenSSL’s default, which already leads with the hybrid group and tracks the library’s state of the art. An explicit list is a snapshot that ages the way the 2018 list aged.
Verified Server Block
listen 443 ssl(plus IPv6)http2 onssl_protocols TLSv1.2 TLSv1.3(keep 1.2 so non-1.3 clients still get a page)- The hybrid-first
ssl_ecdh_curve ssl_session_cache shared:SSL:10mssl_session_timeout 1dssl_session_tickets off- HSTS
Reload with nginx -t && systemctl reload nginx. Running it through gixy, the NGINX configuration static analyser, produces no findings.
Strict Posture for Controlled Fleets
ssl_protocols TLSv1.3;
ssl_ecdh_curve X25519MLKEM768;
This rejects classical TLS 1.3 and TLS 1.2 clients with handshake failures. Legitimate for internal APIs and machine-to-machine links where both ends are controlled — do not put it on a public website, or every visitor behind a lagging browser or TLS-inspecting middlebox gets an unexplained connection error.
Performance
ML-KEM-768 public keys serialize to 1206 bytes versus 44 for X25519, and the ciphertext is comparable — roughly 1.1 KB extra in each direction, once, at connection setup. After the handshake it is ordinary AES-GCM with no throughput difference; lattice arithmetic is fast enough to disappear next to the RSA or ECDSA signature in the same handshake.
One number worth noting: a post-quantum ClientHello exceeds 1500 bytes and spans multiple packets, so on paths with broken middleboxes or MTU black holes it can surface as connections that hang rather than fail cleanly — rare, and a pre-existing defect in that path. Cached sessions skip the exchange entirely, which argues for the session cache line.
QUIC/HTTP/3
QUIC uses the TLS 1.3 handshake for key agreement, so the same group negotiation applies. Confirmed with listen 443 quic; — a QUIC client offering only X25519MLKEM768 completed the handshake and negotiated ALPN h3. Existing HTTP/3 deployments get the post-quantum upgrade with no separate work.
Certificates
RSA/ECDSA signatures are equally vulnerable to Shor’s algorithm, but signature forgery is not a harvest-now-decrypt-later problem — a 2035 quantum computer cannot retroactively forge a signature on a 2026 handshake.
OpenSSL 3.5 supports ML-DSA (FIPS 204) and NGINX will serve such certificates (tested with openssl genpkey -algorithm ML-DSA-65, handshake completes with Peer signature type: mldsa65). But no public CA issues ML-DSA certificates and no browser trusts them — deploying publicly yields certificate errors for every visitor. An ML-DSA-65 certificate is 5522 bytes in DER versus 793 for RSA-2048, sent on every full handshake. Use ML-DSA only where both ends are owned, and get key exchange right first.
Troubleshooting
| Symptom | Cause / Fix |
|---|---|
nginx -V shows OpenSSL below 3.5 |
No configuration helps; install a build linked against 3.5+. |
nginx -t fails with SSL_CTX_set1_curves_list("...") failed |
Group name unknown to the TLS library — predates ML-KEM or misspelled. It is X25519MLKEM768; OpenSSL does not accept the IANA form x25519_mlkem768 here. |
$ssl_curve still shows a classical group |
Something in front (CDN, cloud load balancer, reverse proxy) is terminating TLS; test the origin directly. |
| Certain clients now fail | A list was pinned without a classical fallback; re-add the classical groups after the hybrid one. |
| Handshakes hang rather than fail | Suspect MTU or a middlebox choking on a multi-packet ClientHello; reproduce with -groups X25519. |
| Upgraded the package but nothing changed | NGINX loads its TLS library at process start; a reload keeps the master process, so use systemctl restart nginx. |
Ongoing Assurance
Configurations drift and hardening guides get copy-pasted. Scheduled gixy scans across every host, tied to live NGINX runtime metrics, catch the kind of change nobody notices until an audit. The tooling is drop-in compatible with the deprecated nginx-amplify-agent (EOL January 2026).
Bottom Line
The hard part was never enabling post-quantum key exchange — it was noticing that a line of good advice from 2018 had quietly turned it off. Check nginx -V, probe with openssl s_client -groups X25519MLKEM768, put X25519MLKEM768 at the front of any ssl_ecdh_curve or delete the directive, and watch $ssl_curve as adoption climbs on its own, because the clients are already asking.
Read the full article: NGINX Post-Quantum TLS: X25519MLKEM768 with OpenSSL 3.5
