In my previous post I made the case that the only post-quantum protection that counts is the algorithm your connection actually negotiates. This post is what happened when I checked my own laptop.
The setup could not be more favorable. GitHub announced post-quantum
SSH key exchange in September 2025
,
rolling out the hybrid sntrup761x25519-sha512 algorithm to github.com
SSH endpoints from September 17, 2025. My Mac is a current, fully-patched
Apple Silicon machine running macOS 26.4:
$ uname -a
Darwin host.local 25.4.0 Darwin Kernel Version 25.4.0: Thu Mar 19 19:31:09 PDT 2026; root:xnu-12377.101.15~1/RELEASE_ARM64_T8132 arm64
$ sw_vers
ProductName: macOS
ProductVersion: 26.4.1
BuildVersion: 25E253
$ system_profiler SPSoftwareDataType | grep "System Version"
System Version: macOS 26.4.1 (25E253)
It ships OpenSSH 10.2, which supports both post-quantum hybrids and prefers them by default. Post-quantum server, post-quantum client. What could go wrong?
The Check
$ ssh -v git@github.com 2>&1 | grep -E "remote software|kex: algorithm"
debug1: Remote protocol version 2.0, remote software version bc7d36d
debug1: kex: algorithm: ecdh-sha2-nistp256
ecdh-sha2-nistp256 is a purely classical elliptic-curve exchange. No
post-quantum protection. Any recorded session is harvestable.
Aside: the bc7d36d banner is a build identifier, not a version. GitHub
runs its own SSH implementation (you may also see babeld-... banners
from different frontends), so the banner tells you nothing useful. The
kex line is the signal.
The Investigation
SSH negotiation picks the first algorithm in the client’s preference list that the server also offers. So the first question is: what does my client actually prefer?
1. The effective client preference list
$ ssh -G git@github.com | grep -i kexalgorithms
kexalgorithms ecdh-sha2-nistp256,mlkem768x25519-sha256,sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256
There is the smoking gun. This is exactly the stock OpenSSH 10.2 default
order – post-quantum hybrids first – with one anomaly:
ecdh-sha2-nistp256 has been prepended to the front. That pattern, one
algorithm bolted onto otherwise-untouched defaults, is the signature of a
KexAlgorithms ^... directive somewhere (the ^ operator means “move to
the front of the defaults”).
2. Confirm the client is capable
$ which ssh && ssh -V
/usr/bin/ssh
OpenSSH_10.2p1, LibreSSL 3.3.6
$ ssh -Q kex | grep -E 'mlkem|sntrup'
sntrup761x25519-sha512
sntrup761x25519-sha512@openssh.com
mlkem768x25519-sha256
Stock Apple binary, current version, both post-quantum hybrids compiled in. The client is fully capable.
3. Confirm the server is offering
ssh -vv shows both sides’ KEXINIT proposals:
$ ssh -vv git@github.com 2>&1 | grep -A2 "peer server KEXINIT"
debug2: peer server KEXINIT proposal
debug2: KEX algorithms: sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,kex-strict-s-v00@openssh.com
debug2: host key algorithms: ssh-ed25519,ecdsa-sha2-nistp256,rsa-sha2-512,rsa-sha2-256,ssh-rsa
GitHub offers sntrup761x25519-sha512 first, exactly as their 2025
announcement promised. (Note GitHub offers sntrup761 rather than the
NIST-standardized mlkem768x25519-sha256; per their blog post, FIPS
constraints also mean their US-region Enterprise Cloud gets no
post-quantum kex at all.) The server is doing its part.
So: capable client, willing server, classical result. The problem is the
client-side ordering. But my ~/.ssh/config has no KexAlgorithms line:
$ grep -ri kexalgorithms ~/.ssh/config /etc/ssh/ssh_config /etc/ssh/ssh_config.d 2>/dev/null
$
Nothing. Something outside the usual files is injecting that preference.
4. Finding the injection point
Recent macOS wires a crypto policy into the SSH configuration:
$ ls -l /etc/ssh/crypto.conf
lrw-r--r-- 1 root wheel 17 Aug 17 2025 /etc/ssh/crypto.conf -> crypto/apple.conf
$ cat /etc/ssh/crypto.conf /etc/ssh/crypto/* 2>/dev/null | grep -i -B1 kex
Ciphers ^aes128-gcm@openssh.com,aes256-gcm@openssh.com
KexAlgorithms ^ecdh-sha2-nistp256
--
Ciphers ^aes128-gcm@openssh.com,aes256-gcm@openssh.com
KexAlgorithms ^ecdh-sha2-nistp256
--
HostKeyAlgorithms ecdsa-sha2-nistp256-cert-v01@openssh.com,sk-ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp256,sk-ecdsa-sha2-nistp256@openssh.com
KexAlgorithms ecdh-sha2-nistp256
Found it. /etc/ssh/crypto.conf is a symlink to Apple’s crypto policy
crypto/apple.conf, which contains:
KexAlgorithms ^ecdh-sha2-nistp256
One line. The ^ prepends the classical NIST P-256 exchange to the
front of the defaults on every SSH connection from a default macOS
install. (The third block in the output is the stricter fips.conf
profile sitting unused alongside it; the symlink decides which applies.)
The full causal chain:
Apple ships OpenSSH 10.2 with ML-KEM support, then Apple’s own crypto policy prepends
ecdh-sha2-nistp256, so every connection prefers the classical exchange whenever the server offers it. GitHub offers it as a fallback. Post-quantum protection silently lost, despite both endpoints being fully capable.
To be precise about severity: this is a default downgrade, not a
capability restriction. The ^ operator reorders preferences; it
removes nothing. Every post-quantum algorithm remains compiled in and
usable, and anyone who knows can override the policy at any scope –
per connection (ssh -o KexAlgorithms=sntrup761x25519-sha512 ...) or in
~/.ssh/config as shown below. The adjacent fips.conf shows what a
true restriction looks like: a hard KexAlgorithms ecdh-sha2-nistp256
list with no operator, under which post-quantum negotiation would be
impossible without editing system files. Only the symlink target
separates the two.
But do not mistake mild for harmless. A hard restriction fails loudly; a preference reorder succeeds silently with weaker cryptography – which is precisely how it went unnoticed on three of my machines. And the population who know to specify post-quantum algorithms explicitly is a rounding error. Harvest-now-decrypt-later exposure is determined by what defaults do, because defaults are what runs.
There is an irony here. FIPS alignment is presumably the motivation for preferring the NIST P-256 exchange – and FIPS is the same reason GitHub withholds post-quantum kex from its US Enterprise Cloud region. The same compliance logic, applied at both ends, and the security outcome is a downgrade.
The Fix
Do not edit apple.conf; OS updates will restore it. Instead, override
in ~/.ssh/config, which OpenSSH reads before /etc/ssh/ssh_config,
and for this option the first obtained value wins. It must be an explicit
list (not ^/+ modifiers) so it fully replaces the injected ordering:
# ~/.ssh/config
Host *
KexAlgorithms mlkem768x25519-sha256,sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com,curve25519-sha256,ecdh-sha2-nistp256
Keeping ecdh-sha2-nistp256 at the tail preserves compatibility with any
FIPS-only endpoints.
Before and after
# Before
$ ssh -G git@github.com | grep -i kexalgorithms
kexalgorithms ecdh-sha2-nistp256,mlkem768x25519-sha256,sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256
# After
$ ssh -G git@github.com | grep -i kexalgorithms
kexalgorithms mlkem768x25519-sha256,sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com,curve25519-sha256,ecdh-sha2-nistp256
# Verify the negotiated result
$ ssh -v git@github.com 2>&1 | grep "kex: algorithm"
debug1: kex: algorithm: sntrup761x25519-sha512
The negotiated algorithm is now sntrup761x25519-sha512 – GitHub’s
offered hybrid – because my client’s first preference (mlkem768...)
is not in GitHub’s offer, so negotiation falls to the first mutual match.
Still a hybrid post-quantum exchange, still protected against harvest
now, decrypt later.
Not Just One Machine – And a Twist
To confirm this was not a quirk of one install, I ran the same checks across my fleet: an Intel machine on macOS Sonoma patched through 2025, an Apple Silicon (M1-family) machine still on Sonoma 14.5 from mid-2024 that has not taken recent updates, and a brand-new Apple Silicon machine with a stock, fresh install of macOS 26.5.2 (never upgraded, never MDM-managed).
| Initial survey | Mac 1 (upgraded) | Mac 2 (Intel, upgraded) | Mac 3 (then on 14.5) | Mac 4 (fresh install) |
|---|---|---|---|---|
| macOS | 26.4.1 (25E253) | Sonoma 14.7.6 (23H626) | Sonoma 14.5 (23F79) | 26.5.2 (25F84) |
| OpenSSH | 10.2p1 | 9.9p2 | 9.6p1 | 10.2p1 |
| PQ kex compiled in | sntrup + ML-KEM | sntrup + ML-KEM | sntrup only | sntrup + ML-KEM |
/etc/ssh/crypto.conf |
-> apple.conf (Aug 2025) |
-> apple.conf (May 2025) |
absent | absent |
| Injected preference | ^ecdh-sha2-nistp256 |
^ecdh-sha2-nistp256 |
none | none |
| Negotiated vs github.com | ecdh-sha2-nistp256 |
ecdh-sha2-nistp256 |
sntrup761x25519-sha512@openssh.com |
sntrup761x25519-sha512 |
Mac 2, patched through 2025, has the policy and negotiates classical:
$ ssh -G git@github.com | grep -i kexalgorithms
kexalgorithms ecdh-sha2-nistp256,sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com,mlkem768x25519-sha256,curve25519-sha256,...
$ ssh -v git@github.com 2>&1 | grep "kex: algorithm"
debug1: kex: algorithm: ecdh-sha2-nistp256
Mac 3, at that point still on mid-2024’s 14.5, had no crypto policy at all. Its OpenSSH 9.6 is old enough that it predates ML-KEM and even the non-suffixed sntrup name – yet with stock defaults and nothing prepended, it negotiated post-quantum with GitHub (hold that thought; this machine returns shortly):
$ ssh -Q kex | grep -E 'mlkem|sntrup'
sntrup761x25519-sha512@openssh.com
$ ls -l /etc/ssh/crypto.conf 2>/dev/null
$
$ ssh -v git@github.com 2>&1 | grep "kex: algorithm"
debug1: kex: algorithm: sntrup761x25519-sha512@openssh.com
And here is the result that broke my working theory. Mac 4 is the newest machine of the four – newer macOS than Mac 1, same OpenSSH 10.2 – yet it has no crypto policy and negotiates post-quantum out of the box:
$ sw_vers -productVersion
26.5.2
$ ls -l /etc/ssh/crypto.conf 2>/dev/null
$
$ ssh -G git@github.com | grep -i kexalgorithms
kexalgorithms mlkem768x25519-sha256,sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com,curve25519-sha256,...
$ ssh -v git@github.com 2>&1 | grep "kex: algorithm"
debug1: kex: algorithm: sntrup761x25519-sha512
The Controlled Experiment
Cross-machine comparisons carry confounds – different hardware, different histories. So I converted Mac 3 into a controlled experiment: run the full check, apply the pending Sonoma update (14.5 to 14.8.7), run the check again. Same machine, one variable.
Before, on 14.5 (OpenSSH 9.6, no crypto policy):
## RESULT: negotiated key exchange with github.com
debug1: kex: algorithm: sntrup761x25519-sha512@openssh.com
POST-QUANTUM: YES - this connection resists harvest-now-decrypt-later
After, on 14.8.7 (OpenSSH 9.9):
## macOS SSH crypto policy
lrw-r--r-- 1 root wheel 17 Apr 30 18:20 /etc/ssh/crypto.conf -> crypto/apple.conf
--- policy contents (Kex lines) ---
/etc/ssh/crypto.conf:KexAlgorithms ^ecdh-sha2-nistp256
/etc/ssh/crypto/apple.conf:KexAlgorithms ^ecdh-sha2-nistp256
/etc/ssh/crypto/fips.conf:KexAlgorithms ecdh-sha2-nistp256
## RESULT: negotiated key exchange with github.com
debug1: kex: algorithm: ecdh-sha2-nistp256
POST-QUANTUM: NO - classical key exchange negotiated
One routine security update flipped the machine from post-quantum to classical. Direct causal proof: the Sonoma update stream delivers the crypto policy, and the policy causes the downgrade.
Savor the irony in full. The same update upgraded OpenSSH from 9.6 to 9.9, adding ML-KEM support – the NIST-standardized post-quantum algorithm – to the client. The machine’s post-quantum capability went up. Its post-quantum result went from YES to NO.
And note the file date: the symlink is stamped April 30 – the build date of the 14.8.7 update package, in 2026. This matters for what comes next.
What Actually Predicts the Downgrade
Assemble everything. Downgraded: Macs 1 and 2 (upgraded in place through 2025), and now Mac 3 (patched to current Sonoma). Post-quantum: Mac 3 before its update, and Mac 4, a fresh macOS 26.5.2 install. The filesystem evidence explains the split.
On Mac 4, the crypto policy mechanism is entirely absent – no symlink,
no crypto/ directory, nothing:
$ ls -la /etc/ssh/crypto* 2>/dev/null; echo "exit: $?"
exit: 1
On Mac 1 (macOS 26, upgraded), the mechanism is frozen in time:
$ ls -la /etc/ssh/crypto* 2>/dev/null; echo "exit: $?"
lrw-r--r-- 1 root wheel 17 Aug 17 2025 /etc/ssh/crypto.conf -> crypto/apple.conf
/etc/ssh/crypto:
total 16
drwxr-xr-x 4 root wheel 128 Aug 17 2025 .
drwxr-xr-x 15 root wheel 480 May 12 00:40 ..
-rw-r--r-- 1 root wheel 344 Aug 17 2025 apple.conf
-rw-r--r-- 1 root wheel 713 Aug 17 2025 fips.conf
exit: 0
The parent /etc/ssh directory was modified in May 2026 – macOS 26
updates actively maintain this directory – yet every crypto policy
file is untouched since August 17, 2025. And on Mac 3, the freshly
delivered 14.8.7 policy files carry an April 2026 build date.
Put together, Apple’s crypto policy lives differently in two release streams:
- Sonoma (and its security-update stream): actively shipped. The 14.8.7 package, built April 2026, installs the policy today. Any Sonoma machine taking current security updates gets the downgrade.
- macOS 26: neither shipped nor removed. Fresh installs do not receive the mechanism at all. Upgraded machines keep whatever Sonoma/Sequoia-era copy they carried in, untouched by macOS 26 updates – an orphaned artifact riding along.
Which yields the deciding rule, and it is stranger than any version check: your post-quantum posture against GitHub depends on the path your Mac took to get here. A fresh macOS 26 install negotiates post-quantum out of the box. The same OS reached by upgrade does not. A fully patched, currently supported Sonoma machine does not – and its own update stream is what took the capability away, in the very package that added ML-KEM to its OpenSSH. Version numbers do not predict it. Patch level does not predict it. Only the negotiated line tells you.
Machines carrying the policy need the ~/.ssh/config override. And to
be clear: keep patching – OS updates fix far worse than this. Just
verify afterwards, because as Mac 3 demonstrates, an update can silently
change the answer.
One maintenance note: an explicit list does not inherit future defaults. When a macOS or OpenSSH update adds new key exchange algorithms, this pinned list will not pick them up automatically. Revisit it after major updates.
Lessons
- Version numbers lie by omission. OpenSSH 10.2 on both my Mac and in GitHub’s capability list meant nothing; the negotiated line was classical.
- Platform crypto policies are part of your threat surface. A security-motivated, vendor-managed one-liner downgraded every SSH connection from this machine, silently, with no warning.
ssh -Gis the tool. It shows the effective configuration after all includes, policies, and defaults are merged – the list that negotiation actually uses.- Verify negotiation, not capability. The only line that matters is
kex: algorithm:inssh -voutput. Check it against every endpoint you care about. Harvest now, decrypt later only fails if that line says a post-quantum hybrid. - Re-verify after every update. One routine security update flipped a machine from post-quantum to classical while simultaneously adding ML-KEM to its OpenSSH. Yesterday’s verified result does not survive today’s patch.
The whole investigation took four commands. Run them against your own endpoints today; the answer may not be what you assume.
Run It Yourself
I have packaged the full check as a single script
(view the gist
)
that gathers the OS version, OpenSSH version, effective kex preferences,
crypto policy state, GitHub’s server offer, and the negotiated result.
The output identifies each run by short hostname and timestamp so
multiple systems can be compared; prefix with NOHOST=1 to omit the
hostname when sharing output publicly:
curl -sL https://gist.githubusercontent.com/ronaldbradford/b2e0a1d6ee543cf85297ee00bba32365/raw/check-pq-ssh.sh | bash
I would genuinely like to see results from other configurations –
especially ones that settle the open question above: machines upgraded
in place versus fresh installs (with your crypto.conf state and OS
version), Sequoia 15.x to fill the gap between Sonoma and 26,
MDM-managed machines, and Linux distributions with their own crypto
policies (crypto-policies on RHEL/Fedora behaves similarly in spirit).
Post your output in the comments.
References
- GitHub: Post-quantum security for SSH access on GitHub (September 2025)
- OpenSSH: Post-Quantum Cryptography
- NIST FIPS 203: ML-KEM (August 2024)