Why My Mac Was Not Using Post-Quantum SSH With GitHub (And How I Fixed It)

Why My Mac Was Not Using Post-Quantum SSH With GitHub (And How I Fixed It)

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

  1. 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.
  2. 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.
  3. ssh -G is the tool. It shows the effective configuration after all includes, policies, and defaults are merged – the list that negotiation actually uses.
  4. Verify negotiation, not capability. The only line that matters is kex: algorithm: in ssh -v output. Check it against every endpoint you care about. Harvest now, decrypt later only fails if that line says a post-quantum hybrid.
  5. 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

Tagged with: Security Cryptography

Related Posts

Q Day Is Coming: A Plain-English Guide to Post-Quantum Cryptography

Every time you see the padlock in your browser, push code over SSH, or your application connects to a database, TLS or SSH is quietly doing two jobs. First, it proves you are talking to the real server and not an impostor.

Read more

Your Attack Vector Extends Beyond Production Systems

A common data security issue is the unprotected copying of production data to non-production environments without any redaction, masking, or filtering. This practice poses a serious risk. A malicious actor will target the weakest link in your infrastructure, including non-production accounts and the developer systems accessing them.

Read more

Weekly Musings – May 20, 2022

The Linux Foundation came across my reading path two separate times this week. As I continue to re-establish my larger footprint solely in the open-source ecosystem Setting an Open Source Strategy is a detailed report for any business to identify the potential return on investment (ROI) of participating in the open-source ecosystem.

Read more