Enabling quantum-safe authentication in OpenSSH
mldsa44-ed25519, which enables quantum-safe authentication.
This is still experimental and not enabled by default, but it can be configured in a few simple steps.
OpenSSH pioneered adding support for quantum-safe key exchanges. They added support for a
combination of Streamlined NTRU Prime
\(4591^{761}\) and X25519 already in their release 8.0
back in April 2019. That is more than two years before NIST selected CRYSTALS-Kyber as the quantum-safe KEM to be standardized,
and five years before the final FIPS 203 document
standardizing ML-KEM. Then, in April 2022, they made the
quantum-safe KEX the default one. In September 2024 they
added the currently used hybrid mlkem768x25519-sha256, as defined in
draft-ietf-sshm-mlkem-hybrid-kex-10,
and one release later, in April 2025, they made it the
default.
This is very good! In practice, this means that ssh connections with modern versions of OpenSSH (≥9.0) are not vulnerable to “harvest now, decrypt later” attacks: first thanks to Streamlined NTRU Prime, and since 10.0 thanks to ML-KEM.
Forward secrecy #
The key exchange is just one part of the full story here. SSH is a complex protocol that provides authenticated encryption to send commands (or data) to a remote server over an insecure channel (e.g., over the Internet). Every single time you connect to a remote server, a new ephemeral key is generated just for that session. A future complete compromise of the long-term secrets in the server (or client) does not compromise past ssh sessions. This property is known as forward secrecy.
Authenticating the server #
However, we should make sure our client is generating this ephemeral key with the right server and not with a machine controlled by an attacker. This is achieved by the client authenticating the server when establishing the connection. OpenSSH supports a few different methods for authenticating the server, but the default one is based on trust on first use (known as TOFU).
Every OpenSSH server has one or more host key pairs stored, typically, in /etc/ssh/ssh_host_*_key
(where * can be rsa, ecdsa, or ed25519). During the key exchange the server sends its public
host key and a signature over a hash of the full exchange. This happens before the client does
anything else (e.g., entering a password to authenticate ourselves against the server). In TOFU
mode, the first time we connect to a server we are warned with something like this:
The authenticity of host 'aur.archlinux.org (209.126.35.78)' can't be established.
ED25519 key fingerprint is: SHA256:RFzBCUItH9LZS0cKB5UE6ceAYhBD5C8GeOBip8Z11+4
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])?
It is the client’s responsibility to check and verify that the fingerprint of the public key
matches the expected one. Failing to do so can result in the client connecting to a server controlled
by an attacker. In this case, the right thing to do would be to go to https://aur.archlinux.org/ and check
that the provided fingerprint matches the one shown in the warning. If they match, we type yes and
press enter.
Warning: Permanently added 'aur.archlinux.org' (ED25519) to the list of known hosts.
And we can verify that not only that entry was added to ~/.ssh/known_hosts, but the other public
keys as well.
$ tail -n 3 .ssh/known_hosts
aur.archlinux.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEuBKrPzbawxA/k2g6NcyV5jmqwJ2s+zpgZGZ7tpLIcN
aur.archlinux.org ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDKF9vAFWdgm9Bi8uc+tYRB(snip)
aur.archlinux.org ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABB(snip)
How did this happen? Is it a security vulnerability? Short answer: no. This is exactly what happens step by step:
- Client and server negotiate one host key algorithm, Ed25519 in this case, since it’s first in my
client’s default preference list. The server signs the exchange hash with that key only, we get the
TOFU prompt for that one fingerprint, we say
yes, and that one key is written to the known_hosts file. Then we authenticate ourselves (more about this later!) and log in. - Now, post-auth, the server sends a global request
hostkeys-00@openssh.comcontaining all of its host keys: the RSA one, the ECDSA one, and whatever else is configured. - The client picks out the keys it doesn’t already have and sends back
hostkeys-prove-00@openssh.comwith those key blobs. The server must sign, with each corresponding private key, a blob consisting of the string"hostkeys-prove-00@openssh.com", the session identifier, and the public key itself. - Client verifies each signature. The ones that check out get appended to the known_hosts file. Keys the server stopped advertising get deleted.
So the extra entries aren’t unverified in the cryptographic sense: the server proved possession of each private key, bound to this session ID so the proofs can’t be replayed elsewhere. That step exists specifically to stop a server from injecting keys it doesn’t control into your known_hosts, which would otherwise let a third party impersonate that host later.
But don’t take my word for it, check this on your own. Remove all the lines added to
~/.ssh/known_hosts and repeat the same experiment, only this time passing the verbose flag -v to
the ssh command.
You will see something like this:
(snip)
debug1: client_input_hostkeys: searching /home/iyan/.ssh/known_hosts for aur.archlinux.org / (none)
Learned new hostkey: RSA SHA256:(snip)
Learned new hostkey: ECDSA SHA256:(snip)
(snip)
Authenticating ourselves (the client) #
Once our ssh client is convinced that it is talking to the right server, it is the server’s turn to check whether or not it should allow our client to establish a connection. Typically, this is done either with a password or with another pair of keys and a proof of possession (of the private key). The first option is boring and it’s already quantum-safe, so I’ll focus on the public key method. This is what happens:
- In the query phase the client offers its public key without any signature.
- The server looks up a matching line in the
authorized_keysfile and replies whether that key would be acceptable. - The client unlocks the private key. This is when you are asked for the passphrase to decrypt the private key.
- The client prepares and signs the proof blob, which is a concatenation of a few things that are not so relevant here. The important thing is that this signed proof contains bytes unique to this particular session.
- The server verifies that the signature is valid. It does three checks: 1) key is authorized, 2) the signature is valid under that public key, and 3) the session identifier inside the blob matches the server’s own.
That’s it! So now let’s make both authentications quantum-safe using mldsa44-ed25519.
Before we start #
You need OpenSSH 10.4 or newer on both ends. Nothing below works if either side is older, so check first, on the client and on the server:
$ ssh -V
OpenSSH_10.4p1, OpenSSL 3.6.3 9 Jun 2026
$ ssh -Q key | grep mldsa
ssh-mldsa44-ed25519@openssh.com
ssh-mldsa44-ed25519-cert-v01@openssh.com
If ssh -Q key prints nothing, your OpenSSH does not know about this scheme and there is nothing to
configure. On the server you can check the daemon itself with sshd -V, although in practice both
binaries come from the same package and will report the same version.
Configuring the server #
The first thing we need to do is generate the new mldsa44-ed25519 key pair. As root, run the
following command:
ssh-keygen -t mldsa44-ed25519 -f /etc/ssh/ssh_host_mldsa44_ed25519_key -N ""
This will generate the passwordless host key in the exact same way as the default ones (RSA, ECDSA,
and Ed25519) are generated. This means that when OpenSSH enables mldsa44-ed25519 by default,
you will not have to do anything.
Then create the following config file in /etc/ssh/sshd_config.d/10-enable-pqc-auth.conf:
HostKey /etc/ssh/ssh_host_mldsa44_ed25519_key
HostKeyAlgorithms ^ssh-mldsa44-ed25519@openssh.com
PubkeyAcceptedAlgorithms +ssh-mldsa44-ed25519@openssh.com
All three lines are needed, and they do different things. HostKey only tells the server which key
file to load. It is HostKeyAlgorithms that makes the server actually offer that key during the
key exchange. Leave the second line out and the server will load the key and then never advertise
it, which on the client side looks like this:
debug1: kex: host key algorithm: (no match)
PubkeyAcceptedAlgorithms has nothing to do with host keys: it is the list of signature algorithms
the server will accept when we authenticate ourselves, which is the next step.
^ places the algorithm at the head of the default set, while the character +
appends to the default set. You can remove them if you only want to allow these new
mldsa44-ed25519 keys. Note that by doing that you will prevent most clients from connecting to
your server, as these hybrid keys are not enabled by default in any ssh implementation. Be careful
not to lock yourself out by, for example, keeping an existing ssh connection open.
And, finally, restart the server by running, also as root:
systemctl restart sshd
Configuring the client #
The first thing we need to do is, again, generate the new keys. This time you want to protect your
private key with a passphrase, so it’s the same command as above but without passing the flag -N "".
ssh-keygen -t mldsa44-ed25519
Print the public key and add it to the ~/.ssh/authorized_keys file on the server:
cat ~/.ssh/id_mldsa44_ed25519.pub
Now, because this signature scheme is not enabled by default, we also have to configure the client
to accept it or the authentication with the server will fail. You can do this server by server in
the ~/.ssh/config file, or globally by adding, for example, the config file in
/etc/ssh/ssh_config.d/10-enable-pqc-auth.conf. Notice that this is in ssh_config.d not
sshd_config.d.
HostKeyAlgorithms ^ssh-mldsa44-ed25519@openssh.com
PubkeyAcceptedAlgorithms +ssh-mldsa44-ed25519@openssh.com
These are the same two directives as on the server, but their meaning is mirrored. Here,
HostKeyAlgorithms is our preference list for verifying the server, and it is this list, not the
server’s, that decides which host key ends up being used: the server offers a set, and the client
picks the first entry of its own list that the server also supports. This is why configuring only
the server is not enough: you would still see Ed25519 being negotiated. Likewise,
PubkeyAcceptedAlgorithms is now the list of signature algorithms our client is willing to offer to
authenticate itself.
The client reads the config files when trying to connect to a server, so there is nothing to restart in this case.
~/.ssh/config don’t forget to update the line with IdentityFile to
point to the new key at ~/.ssh/id_mldsa44_ed25519.
Verifying that everything works as expected #
Now it’s time to check that everything is working as expected. Connect to the server passing the
flag -v to print a bunch of debug messages. Pay attention to the following lines:
debug1: loaded pubkey from (snip): MLDSA44-ED25519 SHA256:(snip)
debug1: kex: host key algorithm: ssh-mldsa44-ed25519@openssh.com
debug1: Server host key: ssh-mldsa44-ed25519@openssh.com SHA256:(snip)
debug1: Host (snip) is known and matches the MLDSA44-ED25519 host key.
debug1: Will attempt key: (snip) MLDSA44-ED25519 SHA256:(snip) explicit
debug1: Offering public key: (snip) MLDSA44-ED25519 SHA256:(snip) explicit
debug1: Server accepts key: (snip) MLDSA44-ED25519 SHA256:(snip) explicit
Caveats #
Before you go and replace every key you own, there are a few things worth knowing.
-
It is experimental, and the specification is not final. The key exchange praised at the top of this post rests on solid ground:
mlkem768x25519-sha256is specified in a working group document. The signature scheme is not there yet. It follows draft-miller-sshm-mldsa44-ed25519-composite-sigs, an individual submission sitting at version 00 (June 2026) that thesshmworking group has not adopted. The OpenSSH release notes call the support experimental and ship it disabled by default. If the wire format changes before it settles, you get to regenerate your keys. -
Nothing else speaks it yet. As far as I know, OpenSSH 10.4 is currently the only implementation. Your phone’s ssh app, PuTTY, your CI runner, and your git forge all know nothing about
mldsa44-ed25519. So keep your Ed25519 key inauthorized_keys. The^and+in the configs above already take care of that, but it is worth saying out loud: this is an addition, not a replacement, at least until the rest of the ecosystem catches up. -
The keys and the signatures are big. The host key plus its signature add roughly 3.8 kB to every handshake, and authenticating ourselves adds about the same again. On a normal link this is nothing, and it is a one-off cost per connection rather than per byte transferred. It only starts to matter with very large
authorized_keysfiles or constrained embedded devices. The 2484 bytes per signature are simply the two signatures concatenated: 2420 from ML-DSA-44 plus 64 from Ed25519. That is the whole point of a composite scheme, breaking one of the two is not enough. Note also that ML-DSA-44 is the smallest of the three parameter sets in FIPS 204, at NIST security category 2. Here is a summary of all the sizes:ed25519 mldsa44-ed25519 public key (ssh wire blob) 51 B 1383 B authorized_keysline92 B 1888 B signature (raw) 64 B 2484 B
Bonus: Disabling non-quantum-safe KEX #
As mentioned at the beginning, OpenSSH has been using mlkem768x25519-sha256 by default since
version 10.0 (April 2025). However, the server will still accept connections from clients that do
not support a quantum-safe key exchange, and your client will happily connect to such servers. If
you want to avoid that, create the same config file on both sides:
/etc/ssh/sshd_config.d/10-require-pqc-kex.conf on the server, and
/etc/ssh/ssh_config.d/10-require-pqc-kex.conf on the client.
KexAlgorithms mlkem768x25519-sha256,sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com
sntrup761x25519-sha512 is the
IANA-assigned name, which only exists since OpenSSH 9.9. Peers running 9.0 to 9.8 know the very same
algorithm exclusively under its vendor extension name sntrup761x25519-sha512@openssh.com, so
leaving it out would lock out peers that are, in fact, quantum-safe.
Note that there is no ^ or + here: this list replaces the default one, so everything that is
not quantum-safe (ECDH, plain X25519, finite-field Diffie-Hellman) is gone. Since OpenSSH 10.0, a
warning is shown when connecting to servers vulnerable to “harvest now, decrypt later” attacks. This
change makes your client abort the connection earlier instead of just showing a warning and
continuing with the handshake.
That’s all for now. Give it a try to mldsa44-ed25519 and happy hacking!