Reading a report
All output on this page is real, from 2026-10-15. Servers change, so running it today may give different answers.
The verdict
Section titled “The verdict”The first lines say what was checked, the verdict, why, and the cause code:
✘ github.com:443 (140.82.114.4) Not post-quantum TLS 1.3 works, but when offered X25519MLKEM768 the server asks for X25519 instead. Its TLS library doesn't have ML-KEM, or the groups are limited in config. cause: no-mlkem✔means post-quantum and✘means it isn’t. The address in brackets is the one that was probed.- The cause code is stable, so it’s what scans, the GitHub Action and Flarebox’s history compare. The sentence above it can change between versions.
- When the
Serverheader names the software, it’s shown next to the cause, e.g.server: nginx/1.31.3. That’s what tailors the fixes.
A post-quantum server looks like this. It has more than one address, so each is listed:
✔ cloudflare.com:443 (104.16.133.229) Post-quantum: negotiates X25519MLKEM768 Supports and prefers hybrid ML-KEM key exchange. cause: pq-preferred server: cloudflare
Addresses · 104.16.133.229 pq-preferred · X25519MLKEM768 · 104.16.132.229 pq-preferred · X25519MLKEM768Fixes are numbered in the order to try them. The first is for the stack pqdoctor found, the last is always the Cloudflare Tunnel fallback, and each has the config lines to paste:
✘ nginx.org:443 (3.147.99.198) Not post-quantum: TLS 1.2 only The server doesn't speak TLS 1.3, so it can't do hybrid ML-KEM. cause: tls12-only server: nginx/1.31.3
Fixes 1. Turn on TLS 1.3 Hybrid post-quantum key exchange only exists in TLS 1.3. This server answered with TLS 1.2. # nginx ssl_protocols TLSv1.2 TLSv1.3; # Apache SSLProtocol -all +TLSv1.2 +TLSv1.3 2. nginx: link against OpenSSL 3.5+ and list the PQ group nginx gets PQ from its TLS library. OpenSSL 3.5 or newer is needed (Ubuntu 25.04+, Debian 13+, or an nginx build linked against 3.5). Check with `nginx -V`. ssl_protocols TLSv1.2 TLSv1.3; ssl_ecdh_curve X25519MLKEM768:X25519:prime256v1; 3. Can't upgrade? Cloudflare Tunnel Run cloudflared next to the server. The tunnel from the server to Cloudflare already uses post-quantum key agreement, so the hop that leaves your network is protected even if the server isn't. cloudflared tunnel run <tunnel-name>Authentication
Section titled “Authentication”The ML-DSA check lists the certificate chain the server sends today’s clients: each certificate’s key, what signed it, and its size. Then it says whether the server serves an ML-DSA certificate:
Authentication No ML-DSA certificate Leaf: ec-p256 key, signed ecdsa-sha256. Listing ML-DSA schemes doesn't break it, and offered only ML-DSA it refuses cleanly — what any correct TLS stack does, so this doesn't show whether its stack could serve ML-DSA. The chain is 2737 bytes. ml-dsa: no-cert leaf github.com ec-p256 signed ecdsa-sha256 1009 B ↑ Sectigo Public Server Authenticat… ec-p256 signed ecdsa-sha384 867 B ↑ Sectigo Public Server Authenticat… ec-p384 signed ecdsa-sha384 842 BProbes
Section titled “Probes”Each of the seven probes, with the server’s first reply and how long it took:
Probes · TLS 1.3 with classical groups TLS 1.3: picked X25519 (57 ms) · Offer only an X25519MLKEM768 key share TLS 1.3: asked to retry with X25519 (58 ms) · X25519 share, ML-KEM listed first TLS 1.3: picked X25519 (53 ms) · Old draft group X25519Kyber768Draft00 TLS 1.3: asked to retry with X25519 (52 ms) · PQ ClientHello split across two TCP segments TLS 1.3: asked to retry with X25519 (89 ms) · Classical ClientHello, same size, split TLS 1.3: picked X25519 (103 ms) · P-256 / P-384 only TLS 1.3: picked P-256 (56 ms)Read across, these tell the story: TLS 1.3 works (1), offered only ML-KEM the server asks for X25519 instead (2), and it doesn’t know the Kyber draft either (4). Split hellos are fine (5, 6), so no middlebox is at fault, and it answers P-curves (7) without preferring them. So it’s no-mlkem, not pinned-nist.
Verify it yourself
Section titled “Verify it yourself”Every report ends with an OpenSSL command that offers only X25519MLKEM768. With OpenSSL 3.5+, a post-quantum server’s handshake completes with that group, and any other server’s fails:
openssl s_client -connect github.com:443 -servername github.com -groups X25519MLKEM768 </dev/null 2>&1 | grep -i 'group'--json prints the whole report as one object. These are the fields that matter for automation:
| Field | |
|---|---|
verdict |
pq, pqWithWarning, notPq, noTls13, broken or unreachable |
causeCode |
The cause code |
group |
The PQ group negotiated, if any |
prefersPq |
Picks ML-KEM even when the client’s first key share is classical |
warnings |
E.g. split PQ hellos dropped, or PQ only when offered first |
fixes |
title, detail and snippet for each fix |
fingerprint |
serverHeader, stack, TLS version, cipher |
probes |
Each probe and its reply |
auth |
The certificate check, when it ran |
addresses |
Per-address results, when the name has more than one |
verify |
The OpenSSL command |
Exit status
Section titled “Exit status”For one host: 0 post-quantum, 1 not post-quantum, 2 unreachable or an error.