Post-quantum and TLS

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.

Updated Houssam Hammoudi, CTOTested with OpenSSL 3.5.8, OpenSSH 10.0p2 (alpine:3.22)

On this page
  1. What goes wrong
  2. What the docs say
  3. The secure configuration
  4. Prove it
  5. Mistakes people make
  6. Checklist

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.

bash
#!/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"
done

Build 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.

text
# hosts.txt
www.example.com 443
api.example.com 443
db.internal.example 5432
bastion.example.com 22 ssh

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

RankEndpoint carriesWhy first
1Secrets with a long life: credentials, keys, health, legal, source codeRecorded today, still valuable in 10 years
2Admin and SSH accessOne recorded session can expose many systems
3Service-to-service and database trafficHigh volume, often forgotten
4Public web pages with short-lived dataBrowsers 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.

bash
sh pq-inventory.sh <<LIST
cloudflare.com 443
www.google.com 443
github.com 443
github.com 22 ssh
LIST
text
# 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 -starttls for 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 free

The 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