OpenSSL Quantum-Safe Configuration: Enable PQ TLS on Your Server
OpenSSL 3.5 (released April 2025) is the first upstream release that includes ML-KEM, ML-DSA, and SLH-DSA in the default provider. This means you can enable post-quantum TLS key exchange on your server without third-party providers or patches, just an OpenSSL upgrade and a configuration change.
This guide covers the practical steps: checking your current OpenSSL version, upgrading if needed, configuring Nginx or Apache, and verifying that PQ key exchange is working.
Prerequisites
- OpenSSL 3.5 or newer (check with
openssl version) - Nginx 1.25+ or Apache 2.4.x compiled against OpenSSL 3.5+
- A server running Ubuntu 24.04+ or equivalent (for package availability)
If your OpenSSL is older than 3.5, you have two options:
- Upgrade OpenSSL and recompile/relink your web server
- Use the OQS provider with OpenSSL 3.x (development/testing, not production-recommended)
Check your current setup
# Check OpenSSL version
openssl version
# Need: OpenSSL 3.5.0 or newer
# Check if ML-KEM is available
openssl list -kem-algorithms | grep -i ML-KEM
# Should show: ML-KEM-512, ML-KEM-768, ML-KEM-1024
# Check your Nginx's linked OpenSSL version
nginx -V 2>&1 | grep -i openssl
# Need: built with OpenSSL 3.5+
If any of these checks fail, you need to upgrade before proceeding.
Nginx configuration
Add the X25519MLKEM768 group to your TLS configuration:
# In http {} or server {} block:
ssl_protocols TLSv1.3;
ssl_conf_command Groups X25519MLKEM768:x25519:secp256r1:secp384r1;
ssl_prefer_server_ciphers off;
What this does: The Groups directive tells Nginx to offer X25519MLKEM768 as the preferred key exchange group. Clients that support it (Chrome, Edge, Firefox, Safari on iOS 26+) will negotiate a hybrid post-quantum handshake. Clients that do not support it fall back to classical X25519 or secp256r1.
Important: List X25519MLKEM768 first so it is preferred when both sides support it. The fallback groups (x25519, secp256r1, secp384r1) ensure compatibility with older clients.
Full Nginx server block example
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.3 TLSv1.2;
ssl_conf_command Groups X25519MLKEM768:x25519:secp256r1:secp384r1;
ssl_prefer_server_ciphers off;
# Your normal location blocks...
}
Reload Nginx:
nginx -t && sudo systemctl reload nginx
Apache configuration
For Apache with mod_ssl linked against OpenSSL 3.5+:
# In your VirtualHost or global SSL config:
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
SSLOpenSSLConfCmd Groups X25519MLKEM768:x25519:secp256r1:secp384r1
The SSLOpenSSLConfCmd directive passes configuration directly to OpenSSL. The syntax is the same as Nginx’s ssl_conf_command.
Restart Apache:
apachectl configtest && sudo systemctl restart apache2
Verify it works
After configuring, verify from another machine:
# Test PQ key exchange from a client
openssl s_client -connect your-server.example.com:443 -groups X25519MLKEM768
# Look for "Server Temp Key: X25519MLKEM768" in the output
# If you see "Server Temp Key: X25519" instead, PQ is not active
Browser verification
- Visit your site in Chrome or Edge
- Open DevTools > Security > Overview
- Check the “Key exchange” field
- Should show “X25519MLKEM768” (not “X25519” alone)
Common issues
“Unknown group X25519MLKEM768” — Your OpenSSL is too old. Check openssl version and upgrade to 3.5+.
Nginx fails to reload — The ssl_conf_command directive requires Nginx 1.19+ and OpenSSL 3.x. Older Nginx versions do not support this directive.
Clients still show classical key exchange — The client (browser) must also support X25519MLKEM768. Check the browser version. Chrome 124+ (2024), Edge 124+, Firefox latest, Safari on iOS 26/macOS Tahoe all support it.
Middleboxes break the connection — Some corporate firewalls, IDS/IPS systems, and TLS-inspecting proxies reject the larger ClientHello that PQ negotiation produces. Test without middleboxes first (direct connection to the server) to isolate the issue.
Performance impact
On a typical web server:
- Handshake latency: +1-3ms (ML-KEM-768 key exchange is fast, comparable to X25519)
- Bandwidth: +1.1 KB per TLS handshake (larger key share in ClientHello/ServerHello)
- CPU: Negligible additional load (ML-KEM operations are efficient on modern CPUs)
- Memory: No significant increase
For most web servers, the performance impact is undetectable. Benchmark if you handle more than 10,000 new TLS connections per second, where the cumulative handshake overhead might become measurable.
Generating PQ keys and certificates (experimental)
Enabling PQ key exchange (above) is the immediate priority. PQ certificates (using ML-DSA signatures) are a separate, longer-term migration:
# Generate an ML-DSA-65 key (for testing, not yet usable in public WebPKI)
openssl genpkey -algorithm ML-DSA-65 -out mldsa65.key
# Generate a self-signed certificate with ML-DSA signature
openssl req -new -x509 -key mldsa65.key -out mldsa65.crt -days 365 \
-subj "/CN=pq-test.example.com"
# This certificate will NOT be trusted by browsers (no PQ root CAs in trust stores yet)
# Useful for internal services, testing, and development environments only
PQ certificates in the public WebPKI are expected 2027-2028 when browser trust stores add PQ-capable root CAs.
OQS Provider (for older OpenSSL)
If you cannot upgrade to OpenSSL 3.5 but need PQ for development or testing:
# Install the OQS provider for OpenSSL 3.x
git clone https://github.com/open-quantum-safe/oqs-provider.git
cd oqs-provider
cmake -S . -B build -DOPENSSL_ROOT_DIR=/usr/local/openssl3
cmake --build build
cmake --install build
# Configure OpenSSL to load the OQS provider
# Add to openssl.cnf:
# [provider_sect]
# default = default_sect
# oqsprovider = oqsprovider_sect
# [oqsprovider_sect]
# activate = 1
Caution: The OQS provider is maintained by the Open Quantum Safe project for research and interoperability testing. It is not production-supported in the same way that OpenSSL’s built-in provider is. For production, upgrade to OpenSSL 3.5.
Automation: checking your entire fleet
For infrastructure teams managing many servers:
#!/bin/bash
# pq-check.sh: check PQ support across a list of hosts
while read -r host; do
result=$(echo | openssl s_client -connect "$host:443" -groups X25519MLKEM768 2>/dev/null | grep "Server Temp Key")
if echo "$result" | grep -qi "mlkem"; then
echo "✅ $host: PQ active"
else
echo "❌ $host: classical only"
fi
done < hostlist.txt
Run this periodically to catch regressions (servers that lose PQ support after a config change or upgrade).
FAQ
Do I need OpenSSL 3.5 specifically or will newer work?
OpenSSL 3.5 and newer all include PQ support. If you are on 3.6 or later, it works the same way. Avoid 3.4 and older, they do not include ML-KEM in the default provider.
Will this break older clients?
No. The configuration includes fallback groups (x25519, secp256r1). Clients that do not support X25519MLKEM768 negotiate classical key exchange normally. No client is excluded.
Do I need a new TLS certificate for PQ key exchange?
No. PQ key exchange works with your existing RSA or ECDSA certificate. The key exchange algorithm and the certificate signature algorithm are independent. You can have PQ key exchange with a classical certificate (this is the standard deployment in 2026).
How is this different from enabling it on Cloudflare?
If you are behind Cloudflare, they handle the client-facing PQ automatically. You would configure PQ on your origin server to protect the Cloudflare-to-origin leg. If you are NOT behind Cloudflare (direct TLS termination), this guide configures PQ for your clients directly.
Can I use Let’s Encrypt certificates with PQ key exchange?
Yes. Let’s Encrypt certificates (RSA or ECDSA) work perfectly with PQ key exchange. The two are independent. Your existing Let’s Encrypt cert + PQ key exchange configuration gives you hybrid protection today.