Finding classical key exchange in your estate, the secure way
Someone asked "how quantum-ready are we?" and wants a number by Friday. The honest number starts with a list of every endpoint and the key exchange it actually negotiates, and nobody has that list.
The short answer
List every TLS and SSH endpoint, including internal ones. From a client with OpenSSL 3.5 and OpenSSH 9.9 or newer, record the group each endpoint negotiates with a default offer and whether it accepts MLKEM1024. Anything reporting X25519, P-256 or curve25519-sha256 is classical. Fix long-lived secrets first, then everything else.
On this page
What goes wrong
A quantum-readiness project usually starts with a spreadsheet of products and a vendor questionnaire. Both describe what could be configured. Neither tells you what a connection negotiates today.
The endpoints that matter are the ones nobody lists: the database port, the internal API behind the load balancer, the metrics scraper, the SSH jump host, the backup target, the vendor appliance. Each carries data that may still be sensitive in ten years, and each can be recorded now.
The second mistake is a scanner that cannot see the answer. A tool built on an older TLS library never offers ML-KEM, so it reports every server as classical. Or it only checks certificates, which are not what recorded traffic depends on.
What the docs say
Encrypted data remains at risk because of the “harvest now, decrypt later” threat in which adversaries collect encrypted data now with the goal of decrypting it once quantum technology matures.
Source: NIST IR 8547 (initial public draft), Transition to Post-Quantum Cryptography Standards
NIST intends to instead deprecate rather than fully disallow classical key-establishment schemes at the 112-bit security level.
Source: NIST IR 8547 (initial public draft), Transition to Post-Quantum Cryptography Standards
If you received such a warning, it means that the server you connected to did not offer one of the two post-quantum key agreement algorithms that are being standardised for the SSH protocol
Source: OpenSSH, Post-Quantum Cryptography
The NIST draft gives the timeline (112-bit schemes deprecated after 2030, all classical key establishment disallowed after 2035) and the reason. It does not tell you how to find what you run. That part is below.
The secure configuration
The inventory script. It only completes a handshake and disconnects; it
never authenticates. Run it from a machine with OpenSSL 3.5+ and OpenSSH
9.9+, or from a container such as alpine:3.22.
#!/bin/sh
# pq-inventory.sh: report the key exchange each endpoint agrees to.
# Input: one "host port [tls|ssh]" per line on stdin.
printf "%-24s %-5s %-4s %-8s %-26s %s\n" ENDPOINT PORT KIND TLS DEFAULT_OFFER MLKEM1024
while read -r host port kind; do
case "$host" in ''|\#*) continue ;; esac
kind=${kind:-tls}
if [ "$kind" = ssh ]; then
# -n: do not read the host list from stdin. BatchMode: never prompt.
kex=$(ssh -n -v -p "$port" -o BatchMode=yes -o ConnectTimeout=5 \
-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
probe@"$host" true 2>&1 | sed -n 's/^debug1: kex: algorithm: //p' | head -1)
printf "%-24s %-5s %-4s %-8s %-26s %s\n" "$host" "$port" ssh - "${kex:-no answer}" -
continue
fi
# What a modern client (and a browser) gets: OpenSSL's default offer.
out=$(openssl s_client -connect "$host:$port" -servername "$host" </dev/null 2>&1)
ver=$(echo "$out" | sed -n 's/^ *Protocol *: *//p' | head -1)
grp=$(echo "$out" | sed -n 's/^Negotiated TLS1.3 group: //p; s/^Peer Temp Key: \([^,]*\),.*/\1 (classical)/p' | head -1)
# NIST level 5: offer MLKEM1024 alone.
l5=$(openssl s_client -connect "$host:$port" -servername "$host" -groups MLKEM1024 </dev/null 2>&1 \
| grep -q "^Negotiated TLS1.3 group: MLKEM1024" && echo yes || echo no)
printf "%-24s %-5s %-4s %-8s %-26s %s\n" "$host" "$port" tls "${ver:-none}" "${grp:-no answer}" "$l5"
doneBuild the input from what you already have: load balancer listeners, DNS zones, Kubernetes Services of type LoadBalancer, firewall rules, the SSH hosts in your config management. A port scan of your own address ranges finds the rest.
# hosts.txt
www.example.com 443
api.example.com 443
db.internal.example 5432
bastion.example.com 22 sshFor protocols that start in plain text and upgrade (PostgreSQL, SMTP, LDAP),
add -starttls postgres, -starttls smtp or -starttls ldap to the
openssl s_client lines.
Rank what you find:
| Rank | Endpoint carries | Why first |
|---|---|---|
| 1 | Secrets with a long life: credentials, keys, health, legal, source code | Recorded today, still valuable in 10 years |
| 2 | Admin and SSH access | One recorded session can expose many systems |
| 3 | Service-to-service and database traffic | High volume, often forgotten |
| 4 | Public web pages with short-lived data | Browsers already negotiate the hybrid where the server allows it |
Prove it
The test in secure-tests/finding-classical-key-exchange-estate/run.sh runs
the script against four public endpoints from alpine:3.22. These are
public services on 2026-09-24; they will change.
sh pq-inventory.sh <<LIST
cloudflare.com 443
www.google.com 443
github.com 443
github.com 22 ssh
LIST# run 2026-09-24, OpenSSL 3.5.8, OpenSSH_10.0p2
ENDPOINT PORT KIND TLS DEFAULT_OFFER MLKEM1024
cloudflare.com 443 tls TLSv1.3 X25519MLKEM768 no
www.google.com 443 tls TLSv1.3 X25519MLKEM768 yes
github.com 443 tls TLSv1.3 X25519 (classical) no
github.com 22 ssh - sntrup761x25519-sha512 -Four endpoints, four different answers: level 3 hybrid, level 5 on request, classical HTTPS, and a post-quantum SSH hybrid on the same organization's SSH service. That is why the inventory has to be per endpoint.
Mistakes people make
Scanning with an old TLS library
An OpenSSL before 3.5 never offers ML-KEM, so every endpoint looks classical. Print the client version at the top of every report, as the script does.
Inventorying certificates instead of key exchange
Certificate scanners list RSA and ECDSA keys. That is useful later, but recorded traffic depends on the key exchange. Measure that first.
Scanning only the internet-facing side
Internal hops often run older stacks and carry the most sensitive data. Run the same script from inside each network zone.
Treating a green hybrid as done
X25519MLKEM768 is the right answer for browsers. For machine-to-machine traffic and anything under CNSA 2.0, check the MLKEM1024 column too.
Running it once
Upgrades and rollbacks change the answer. Run the inventory on a schedule, keep the dated output, and alert when an endpoint moves back to classical.
Checklist
- Build the endpoint list from load balancers, DNS, Services, firewalls and SSH configs.
- Run the scan from a client with OpenSSL 3.5+ and OpenSSH 9.9+.
- Record the default-offer group and the MLKEM1024 result per endpoint.
- Add
-starttlsfor PostgreSQL, SMTP and LDAP endpoints. - Rank classical endpoints by how long their data stays sensitive.
- Scan from inside every network zone, not only from the internet.
- Re-run on a schedule and keep dated results.
You cannot migrate what you have not measured. The good news: measuring is one handshake per endpoint.
H2-CPQE
Learn it on a live range
Post-quantum TLS, in Edge and Post-Quantum Networking: a real host in your browser, and every objective checked on the machine.
Start freeThe Dome
Want it run for you?
The Dome puts post-quantum TLS, a WAF that blocks, signed DNS and a zero-trust mesh in front of your application. Tell us what you run.
See the Dome