- Published on
Reading OpenSSH 10.4 Closely — Experimental ML-DSA Composite Signatures, the Next Step After the mlkem768 Default, and the Deprecations That Bite Operators
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- Introduction — A Turning Point Dressed as a Bugfix Release
- The Key Exchange Story — From sntrup761 to the mlkem768 Default
- The 10.1 Warning — The Operational Reality WarnWeakCrypto Created
- 10.4 — The Signature Front Opens
- The Current State of Draft -00 — The Numbers Don't Add Up
- The Changes That Bite Operators — 10.0 Through 10.4
- What Not to Do
- A Practical Checklist
- Closing
- References
Introduction — A Turning Point Dressed as a Bugfix Release
OpenSSH 10.4 shipped on July 6, 2026. The release notes introduce themselves modestly — "a number of security fixes as well as general bugfixes and a couple of new features". Most operators will read that far, drop the package into the update queue, and move on.
But one of those "a couple of new features" foreshadows the next five years of the SSH protocol. A post-quantum composite signature bundling ML-DSA-44 with Ed25519 landed, experimentally. With the post-quantum transition of key exchange effectively finished, the signature's turn has now begun.
Yesterday's post, Why PQ Certificates Aren't Here Yet, covered the TLS/WebPKI side of this story — over there, ML-DSA signatures are so large that a detour called Merkle Tree Certificates is being built. SSH's circumstances differ, and so it takes a different road. This post follows that road by working through the original release notes from OpenSSH 10.0 to 10.4, collecting along the way the incompatible changes that bite operators.
First, let me nail down the timeline. All of it comes from the official release notes.
9.0 2022-04 sntrup761x25519-sha512 becomes the default KEX (the start of post-quantum defaults)
9.9 2024-09-19 mlkem768x25519-sha256 added
10.0 2025-04-09 mlkem768x25519-sha256 becomes the default KEX. DSA removed. modp DH removed from server defaults
10.1 2025-10-06 Warning for non-PQ key exchange introduced (WarnWeakCrypto). XMSS removed. Agent socket escapes /tmp
10.2 2025-10-10 Emergency bugfix (ControlPersist making sessions unusable, bz3872)
10.3 2026-04-02 Meaning of empty certificate principals inverted. Compat code for implementations that cannot rekey removed
10.4 2026-07-06 Experimental composite signature ssh-mldsa44-ed25519@openssh.com. seccomp failure made fatal
The Key Exchange Story — From sntrup761 to the mlkem768 Default
According to OpenSSH's post-quantum page, post-quantum key exchange became the default back in 9.0, in April 2022. NIST standardization had not finished at the time, so the project picked sntrup761x25519-sha512 from the NTRU Prime family. 9.9 added mlkem768x25519-sha256, based on the NIST standard (FIPS 203), and it became the default in 10.0.
The 10.0 release notes give four grounds for the swap — it is considered safe against quantum computer attack, it is guaranteed to be no weaker than the previous curve25519-sha256, NIST standardized it, and it is considerably faster than the previous default, sntrup761 ("considerably faster than the previous default" — the project's own wording; the release notes do not present separate benchmark figures).
That "guaranteed to be no weaker" is the heart of the hybrid construction. mlkem768x25519-sha256 performs both ML-KEM-768 and classical x25519 ECDH, then concatenates and hashes the two shared secrets. The IETF document (draft-ietf-sshm-mlkem-hybrid-kex — as of this writing, revision -10 sits in the RFC editor queue) defines it like this.
K = HASH(K_PQ || K_CL)
K_PQ: shared secret from ML-KEM-768 encapsulation (FIPS 203)
K_CL: shared secret from x25519 ECDH (RFC 8731)
If ML-KEM collapses under future cryptanalysis, x25519 remains; if a quantum computer breaks x25519, ML-KEM holds the line. Why take out this much insurance and hurry now? Because of "store now, decrypt later" — the attack of stashing today's traffic and cracking it with a quantum computer later. Once key exchange falls, the entire session is decrypted, so the logic is that key exchange at least must be changed ahead of time, even today, with no cryptographically relevant quantum computer (CRQC) in existence. OpenSSH's PQ page puts the CRQC arrival forecast in a 5-to-20-year range, and writes that many observers expect the mid-2030s.
For protocol-level background — where KEX fits in and what it signs — see SSH Protocol Deep Dive.
The 10.1 Warning — The Operational Reality WarnWeakCrypto Created
Changing a default and moving an ecosystem are different problems. 10.1 (2025-10-06) reached for a more direct instrument. When a client finishes negotiating a non-post-quantum key exchange, it prints this warning.
** WARNING: connection is not using a post-quantum key exchange algorithm.
** This session may be vulnerable to "store now, decrypt later" attacks.
** The server may need to be upgraded. See https://openssh.com/pq.html
The warning is on by default and controlled by a new ssh_config option, WarnWeakCrypto. Operationally, its meaning is simple — the moment you move clients to 10.1 or later, every old SSH server in your organization surfaces right in front of your users. Old network gear, appliances the vendor abandoned, golden images that pinned KexAlgorithms to some ancient value — all of them start warning.
The correct fix is to upgrade the servers. sntrup761 is supported from 9.0 and mlkem768 from 9.9, so if a server is already in that version range, start by checking whether its configuration is turning PQ algorithms off via KexAlgorithms. For servers you truly cannot upgrade, the approach the PQ page recommends is selective, per-host silence.
Match host unsafe.example.com
WarnWeakCrypto no-pq-kex
You will be tempted to slam WarnWeakCrypto no in globally, but the release notes foretell that this option will also govern other weak-crypto warnings in the future. Turning it off globally means pre-emptively turning off future warnings too. Given that training your users to ignore warnings is the worst possible outcome, it is right to scope the silence narrowly.
10.4 — The Signature Front Opens
Unlike key exchange, signatures are not a target for "store now, decrypt later". Forging a signature is something you must pull off in the moment the connection is live, so you cannot retroactively break the authentication of traffic captured today ten years from now. The OpenSSH PQ page states this explicitly, and writes that the only urgency on the signature side is retiring classical signature keys before a CRQC becomes real. So signature migration was a race that could start a few years behind key exchange, and that is exactly how it is playing out.
The omen was in 10.1. Removing XMSS, the experimental hash-based signature scheme, the release notes said this — "We expect to implement a new post-quantum signature scheme in the near future." That near future is 10.4.
10.4 adds experimental support for ssh-mldsa44-ed25519@openssh.com, the composite signature specified in draft-miller-sshm-mldsa44-ed25519-composite-sigs. The draft's author is OpenSSH's own Damien Miller. The construction follows the same philosophy as the key exchange hybrid — post-quantum ML-DSA-44 and classical Ed25519 each sign the same message, and verification is valid only if both pass. As the draft's security considerations put it, forging requires breaking both algorithms.
M' = Prefix || Label || len(ctx) || ctx || SHA512(M)
Prefix = "CompositeAlgorithmSignatures2025"
Label = "COMPSIG-MLDSA44-Ed25519-SHA512"
mldsa_sig = ML-DSA-44.Sign(mldsa_sk, M', ctx=Label)
ed25519_sig = Ed25519.Sign(ed25519_sk, M')
composite_signature = mldsa_sig || ed25519_sig
It is not a default, so using it means adding it by hand to HostKeyAlgorithms, PubkeyAcceptedAlgorithms and friends on both ends. Key generation goes like this.
ssh-keygen -t mldsa44-ed25519
To get a feel for the sizes — Ed25519 has a 32-byte public key and a 64-byte signature. Per FIPS 204, ML-DSA-44 has a 1,312-byte public key and a 2,420-byte signature. The composite public key is the two public keys concatenated, 1,344 bytes, and the composite signature comes to 2,484 bytes. On the TLS side covered yesterday, a single handshake carries several signatures and several public keys, so this size becomes unbearable and a detour called Merkle Tree Certificates is being dug — but an SSH handshake carries just one host key and one signature (the KEX structure of RFC 4253). At roughly 4KB of extra cost per connection, SSH can take the frontal approach of swapping the algorithm in directly, no detour needed.
The Current State of Draft -00 — The Numbers Don't Add Up
Here is one reason to take the "experimental" label seriously. The body of draft -00 (dated June 2, 2026) states the composite signature's size like this.
byte[3309] mldsa_sig
byte[64] ed25519_sig
This 3373-byte combination is henceforth referred to as
"composite_signature".
But the ML-DSA-44 signature that FIPS 204 specifies is 2,420 bytes. 3,309 bytes is the signature size of ML-DSA-65, one level up. The actual OpenSSH 10.4 source (crypto_api.h at tag V_10_4_P1) implements MLDSA44_SIGBYTES 2420, matching FIPS 204 — so the draft's body appears to be the typo. The same document's signature-context section still has a bare "TODO TODO TODO" sitting in it.
This is not meant to mock the draft — a -00 individual submission is normally in exactly this state, and the fact that the document has not even been adopted by a working group yet is itself information. It means the wire format and key format can still change from revision to revision, and the document's status is telling you precisely the same thing OpenSSH pinned down with the word "experimental".
The Changes That Bite Operators — 10.0 Through 10.4
Obscured by the post-quantum story, this release train has also been a run of deprecations. Let me group them by how the failure reveals itself.
The ones that fail at connection time. 10.0 removed the DSA signature algorithm entirely (completing a deprecation process that began with disabling it by default in 2015). The same release dropped finite field DH — diffie-hellman-group* and diffie-hellman-group-exchange-* — from the server defaults. An old client that can do neither ECDH nor PQ cannot connect to the default configuration of a 10.0-or-later server (the client-side defaults were kept, so a new client reaching an old server still works). Also, the behavior of silently falling back to compiled-in groups when a moduli file exists but holds no value in the requested range is gone, so a hand-edited moduli file can now lead to connection failure.
The ones that die mid-session. These are far nastier to debug. 10.3 (2026-04-02) removed bug-compatibility code for implementations that do not support rekeying — the connection establishes normally, and only fails once enough traffic has accumulated to require a rekey. 10.4 tightened the protocol to immediately disconnect a peer that sends non-KEX messages during a post-authentication rekey (the target being implementations that do not follow RFC 4253 section 7.1). What both bite is not the modern OpenSSH but the legacy embedded gear on the other end. When a ticket comes in reading "it connects fine, but it drops whenever I send a big file", suspect this.
The ones that break automation and tooling. 10.1 moved the agent socket from /tmp to under ~/.ssh/agent. It is a security improvement meant to stop processes with restricted /tmp access from grabbing agent keys, but every monitoring and cleanup script globbing the /tmp/ssh- path is invalidated. There are side effects too — the OS's /tmp cleaning no longer sweeps away stale sockets, so ssh-agent gained its own cleanup feature and flags (-U, -u, -uu, and -T to revert to /tmp), and a hostname hash goes into the socket path for NFS homes. In 10.4, the sshd -G config dump changed from all-lowercase to mixed case ("PubkeyAuthentication") — compliance scripts that grep assuming lowercase quietly start returning empty results. The 10.1 IPQoS rework means ToS keywords like lowdelay, throughput, and reliability are now ignored and fall back to the system default QoS.
The ones where authentication behavior changed. The most savorable of 10.3's changes — previously, a certificate with an empty principals section was treated as a wildcard along the principals= option path in authorized_keys, that is, as matching any principal. It was the intended behavior, but it was a trap: if a CA accidentally issued a certificate with empty principals, far from being a useless certificate, it became a skeleton key able to log into every account trusting that CA. From 10.3, empty principals match nothing (the TrustedUserCAKeys path never had this problem). Conversely, if you have an environment leaning on that wildcard behavior, authentication breaks right after the upgrade — which means the dependency was dangerous to begin with, but broken is broken. 10.1's ssh-add now automatically sets an expiry when adding a certificate to the agent, at the certificate's expiry time plus a 5-minute grace period (disable with -N), and pipelines that parked short-lived certificates in a long-lived agent assuming "it'll still be there" now fail after expiry.
Build and deployment environments. From 10.4, failure to enable the seccomp sandbox or NO_NEW_PRIVS on Linux is a fatal error. It used to just log and carry on; now such systems must explicitly disable the sandbox at configure time. The targets here are cases of running sshd on very old kernels or unusual container runtimes.
The next one up, already announced. The 10.1 and 10.2 release notes foretell the deprecation of SHA1 SSHFP DNS records — in a future release SHA1 SSHFP will be ignored, and ssh-keygen -r will generate only SHA256 records. If your organization has planted SSHFP in DNS, regenerating now is cheap (SHA256 SSHFP has been supported since 6.1, in 2012).
On top of that, 10.4 has several security fixes that are themselves a motive to upgrade — an issue where a malicious server could make an sftp host:/path . download write to the wrong location, an issue where a remote-to-remote scp copy could write files into the parent of the destination directory, an issue where internal-sftp's command line was silently truncated after the ninth argument so security options could be discarded, a pre-authentication DoS when GSSAPI authentication is enabled, and a client use-after-free when a server changes host keys during a rekey.
What Not to Do
Don't turn composite signatures on in production. As seen above, the specification is a -00 individual submission draft, its body still has a TODO and a size typo, and OpenSSH itself calls it experimental. If the wire or key format changes, the keys and deployment configuration you build today become debt. Generating a key in a lab and observing interoperability — that is the behavior that fits this stage.
Don't tear up your host keys now over signatures. That signatures carry no retroactive threat is OpenSSH's own explanation. What you need now is not a PQ host key rollout but the rotation muscle — how fast you could turn over the host keys and CA keys of the whole organization once a CRQC comes into view. Without an inventory there is no rotation.
Don't turn the warnings off globally. WarnWeakCrypto is an option slated to govern other weak-crypto warnings going forward. Silence it only per host with a Match block, and manage the list of silenced hosts as a technical debt list in its own right.
Don't pin KexAlgorithms into a golden image and forget it. An algorithm list pinned once for compatibility with old gear makes PQ defaults — and future removals — bypass you entirely. If pinning is truly necessary, it is better to write an expiry date alongside it.
A Practical Checklist
# 1) Check the KEX the client will actually negotiate — is the first entry mlkem768x25519-sha256?
ssh -G host.example.com | grep -i kexalgorithms
# 2) Find anywhere in the config that pins algorithms
grep -riE 'kexalgorithms|hostkeyalgorithms|pubkeyacceptedalgorithms' /etc/ssh/
# 3) Find scripts assuming an agent socket under /tmp (moved to ~/.ssh/agent in 10.1)
grep -rn 'tmp/ssh-' /opt/scripts/
# 4) Find places grepping sshd -G output in lowercase (changed to mixed case in 10.4)
grep -rn 'sshd -G' /opt/scripts/ | xargs -r grep -l 'grep [a-z]'
# 5) Rekey survival test for legacy gear — lower the threshold to force a rekey
ssh -o RekeyLimit=1M legacy-appliance 'dd if=/dev/zero bs=1M count=16' > /dev/null
Beyond this, if your organization uses SSH certificates, inventory now whether your CA issuance pipeline can produce certificates with empty principals (if any pre-10.3 servers remain, that becomes a skeleton key) and where the principals= option in authorized_keys is used. For the whole picture of the post-quantum transition — TLS, VPN, and code signing beyond SSH — it is worth overlaying Post-Quantum Cryptography Migration: The USD 7B Race Against Q-Day.
Closing
OpenSSH's post-quantum transition is proceeding in textbook order. Key exchange first, where the retroactive threat lives — start with sntrup761 before standardization (9.0), swap to mlkem768 once the standard arrives (10.0), push the ecosystem with warnings (10.1) — and then signatures, which carry no retroactive threat, only now setting off with a standards document and an experimental implementation (10.4). Hard on the urgent, careful with the less urgent. This sense of ordering runs on exactly the same principle as the TLS story from yesterday, while taking a far simpler path thanks to SSH's light handshake.
At the same time, this release train has steadily cleared away old things. DSA, modp DH in server defaults, the agent socket in /tmp, patience with implementations that cannot rekey, the wildcard of empty principals. Each removal is reasonable, but the moment each one bites is different — at connect time, at rekey time, right after an upgrade, at script runtime — so being hit without an inventory hurts. Reading the "Potentially-incompatible changes" section of the release notes closely, every release — in the end, that is the summary of this entire post.
References
- OpenSSH 10.4 release notes (2026-07-06)
- OpenSSH 10.3 release notes (2026-04-02)
- OpenSSH 10.1 release notes (2025-10-06)
- OpenSSH 10.0 release notes (2025-04-09)
- OpenSSH: Post-Quantum Cryptography — the warning text and FAQ
- draft-miller-sshm-mldsa44-ed25519-composite-sigs — the ML-DSA 44/Ed25519 composite signature spec
- draft-ietf-sshm-mlkem-hybrid-kex — the mlkem768x25519-sha256 spec (RFC editor queue)
- NIST FIPS 204 — the ML-DSA standard (key and signature size tables)
- openssh-portable V_10_4_P1 — the MLDSA44 constants in crypto_api.h and ssh-mldsa-eddsa.c
- Why PQ Certificates Aren't Here Yet — the same story on the TLS/WebPKI side (related post)