Skip to content

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 first lines say what was checked, the verdict, why, and the cause code:

pqdoctor github.com
✘ 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 Server header 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:

pqdoctor cloudflare.com
✔ 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 · X25519MLKEM768

Fixes 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:

pqdoctor nginx.org
✘ 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>

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:

pqdoctor github.com (continued)
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 B

Each of the seven probes, with the server’s first reply and how long it took:

pqdoctor github.com (continued)
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.

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:

Terminal window
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

For one host: 0 post-quantum, 1 not post-quantum, 2 unreachable or an error.