Read this first
On current stacks this is usually a verification job rather than a configuration job: OpenSSL 3.5 and Go 1.24 both offer X25519MLKEM768 by default. The two things that actually go wrong are an explicit configuration that silently turns it back off, and a middlebox that cannot cope with a ClientHello spanning more than one TCP segment. Check what you negotiate today before you change anything.
Check before you configure
The most common outcome of this task is discovering it was already done. Start by asking the endpoint what it negotiates, rather than reading your config and inferring.
- From the command line:
openssl s_client -connect example.com:443 -tls1_3and look at the negotiated group in the output. You need an OpenSSL 3.5 or newer client for it to offer the hybrid group in the first place. - From the browser, one domain at a time: the post-quantum TLS check walks through reading the result.
- Across an estate: qScan reads the configuration, and qProbe handshakes the endpoints you own.
If the answer is X25519MLKEM768, you are done for that endpoint and the work is elsewhere. If it is X25519 or P-256, read on.
OpenSSL 3.5 and newer
OpenSSL 3.5 changed the default TLS supported-groups list to include and prefer hybrid post-quantum groups, and changed the default key shares to offer X25519MLKEM768 alongside X25519. The default list is:
?*X25519MLKEM768 / ?*X25519:?secp256r1 / ?X448:?secp384r1:?secp521r1 / ?ffdhe2048:?ffdhe3072
The line that turns it off
If your application or config file sets the group list explicitly, you have overridden that default, and unless you added X25519MLKEM768 yourself you have disabled post-quantum key exchange. This is by far the most common reason a stack that should negotiate the hybrid group does not. An explicit groups line written in 2021 is now a downgrade.
nginx
nginx exposes the group list through ssl_ecdh_curve, which sets the TLS 1.3 supported_groups extension. Put the hybrid first and keep classical fallbacks after it, so clients that cannot do ML-KEM still connect:
ssl_protocols TLSv1.2 TLSv1.3;ssl_ecdh_curve X25519MLKEM768:X25519:prime256v1;
The fallbacks are not optional politeness. A client that does not offer ML-KEM must still be able to complete a handshake, so a group list containing only the hybrid will lock out everything old. Hybrid means secure if either half holds, and the deployment should mean reachable if either half is supported.
nginx fails silently on an old OpenSSL
nginx needs to be built against OpenSSL 3.5 or newer for this to do anything. Linked against something older, it does not error on an unrecognised group name, it ignores it. The config looks correct, nginx reloads cleanly, and nothing changes. Check nginx -V for the OpenSSL version it was actually compiled against, not the one your package manager reports.
To confirm from the server side, nginx exposes $ssl_curve, which you can log or pass to an application to see the group each client actually negotiated. That turns the rollout into a measurement rather than a hope.
Go
Go 1.24 added X25519MLKEM768 and enabled it by default, sending X25519MLKEM768 and X25519 key shares, for both clients and servers. There is nothing to switch on.
There is, however, something not to do. The default applies only when Config.CurvePreferences is nil. Setting CurvePreferences for any reason, including a well-intentioned hardening pass, replaces the default list and drops the hybrid group unless you name it explicitly. The GODEBUG setting tlsmlkem=0 also reverts the default, which is worth grepping your deployment environment for. If you build in FIPS mode, verify the behaviour rather than assuming it matches the standard build.
The failure you should expect
A hybrid key share is large. Adding ML-KEM-768 to a classical share puts roughly an extra kilobyte into the ClientHello, which frequently pushes it past a single TCP segment. Before post-quantum key exchange, a ClientHello spanning two segments was rare enough that a lot of deployed middleware quietly assumed it never happened.
So some middleboxes, load balancers and TLS-inspecting appliances read only the first segment, fail to find the end of a message they assumed would arrive whole, and either hang or drop the connection. The failure is not in your server and not in the client. It is in something between them, and it presents as a small, stubborn percentage of connections that fail while everything else works. When Cloudflare rolled this out, the larger ClientHello broke roughly 0.05 percent of connections to origins advertising the post-quantum group directly: small, not zero, and concentrated in exactly the enterprise network gear that is hardest to update.
- Roll out behind a measurement, not a maintenance window. You are looking for a fractional failure rate, which a smoke test will not surface.
- Suspect the path, not the endpoints. If it fails from one network and works from another, that is the signature.
- Check anything that parses a handshake: inspecting proxies, older load balancers, some WAFs, and SNI-based TCP routers, which are a common casualty because they route on a field they now have to reassemble to find.
- Do not respond by disabling the hybrid group estate-wide. Find the device. Turning it off returns you to recording traffic that stays decryptable forever.
What this does not fix
Enabling hybrid key exchange on your edge protects the leg between the client and that edge, from the moment you enable it. Three things remain, and the first is the one people miss.
- The origin leg. If a CDN terminates TLS, the connection from the CDN to your origin is a separate handshake with its own algorithms, and it is frequently still classical. This is why public adoption numbers look better than most estates actually are.
- Signatures. The certificate chain is still classical almost everywhere. That matters less urgently: a forged signature needs a quantum computer at the time of the forgery, whereas a recorded key exchange does not. See ML-DSA vs SLH-DSA for the signature side.
- Everything that is not TLS. Data at rest, backups, message brokers, SSH, code signing, and the encrypted secret already sitting in your git history. A source-code fix moves you forward from today and does nothing for what was already captured, which is the whole harvest-now-decrypt-later problem.
TLS is the surface everyone starts with because it is the easiest to change and the easiest to measure. It is rarely the surface carrying the most unrecoverable exposure. Once this is done, the next question is what else in your estate pins itself to pre-quantum cryptography, which is what a cryptographic inventory answers.