SSH Security Best Practices 2026: OpenSSH 10.6, ML-KEM and FIDO2 Keys

Oct 31, 2026 · 14 min · Ajay Kumar

I open more SSH sessions than I would like to admit for someone who has spent fourteen years telling teams to stop logging into servers. Most SSH security best practices you find in search results were written for OpenSSH 7 or 8, and the ground has moved. Between April 2025 and October 2026 OpenSSH shipped seven releases: ML-KEM post-quantum key exchange became the default, DSA was deleted, the server's pre-authentication code moved into its own binary, and the release notes started crediting AI-assisted bug reports. This post covers what changed, which key types and access patterns I would pick today, a hardened sshd_config I validated with sshd -t, and what regreSSHion and the xz backdoor still teach about running sshd as a fleet service.

What changed in OpenSSH 10.x (2025 to 2026)?

OpenSSH 10.0, released on 9 April 2025, did four things that matter operationally. It made mlkem768x25519-sha256, a hybrid of NIST's ML-KEM and classic X25519, the default key agreement. It removed DSA support entirely, finishing a deprecation that started in 2015. It moved the user-authentication code into a new sshd-auth binary, so the pre-authentication attack surface lives in its own address space (9.8 had already split per-connection work into sshd-session). And it dropped finite-field Diffie-Hellman from the server's default list. A small trap: it announces itself as SSH-2.0-OpenSSH_10.0, and the notes warn that scripts matching OpenSSH_1* will misread it.

OpenSSH 10.1 (6 October 2025) made the client print a warning whenever a connection negotiates key exchange that is not post-quantum, controlled by a new WarnWeakCrypto option. It also moved ssh-agent sockets from /tmp into ~/.ssh/agent, so a process that can read /tmp no longer gets ambient access to your keys, and it rejected control characters in command-line usernames to block shell injection through ProxyCommand %r expansion. 10.2 followed four days later to fix a ControlPersist regression.

The 2026 releases were about tightening edges. 10.3 (2 April) changed certificate semantics: a certificate with an empty principals list used to match any user when trusted through an authorized_keys principals= option, and now matches nobody. 10.4 (6 July) added an experimental ML-DSA-44 plus Ed25519 composite signature and made the transport stricter during rekeying. 10.5 (11 August) fixed an agent-locking bug that, while the agent was locked, let a forwarded agent use keys that had destination restrictions. 10.6 (6 October) enabled the hybrid ssh-mldsa44-ed25519 signature algorithm, added WarnWeakCrypto to the server, and disabled the LZ77 compression dictionary after a 2026 paper showed plaintext recovery across multiplexed channels sharing one compression context.

The most interesting paragraph is not a feature. The 10.5 and 10.6 notes say the team now receives a large number of security reports produced by or with AI models, has seen AI-found bugs independently rediscovered by other researchers, and will ship more frequent releases rather than batch fixes. Several 10.5 and 10.6 hardening changes credit Anthropic's Chris Rohlf, in 10.6 explicitly in collaboration with Claude. I covered the wider trend in AI found the bugs; for SSH the practical consequence is simple: your patch latency is now a security control.

OpenSSH security milestones, 2022 to 2026 (evenly spaced, not to scale) 9.0, Apr 2022 sntrup761 PQ kex becomes the default 9.5, Oct 2023 Ed25519 becomes the ssh-keygen default 9.8, Jul 2024 regreSSHion fixed; PerSourcePenalties on 10.0, Apr 2025 ML-KEM hybrid default, DSA removed 10.1, Oct 2025 warns on non-PQ kex; agent socket in ~/.ssh 10.3, Apr 2026 empty cert principals now match nobody 10.5, Aug 2026 AI bug reports, faster release cadence 10.6, Oct 2026 ML-DSA hybrid keys on; LZ77 coder disabled What ships matters more than upstream: Ubuntu 26.04 has 10.2, Debian 13 has 10.0, RHEL 10 has 9.9. 10.2 (Oct 2025) and 10.4 (Jul 2026) are omitted; see the release notes for every change.
Release dates and changes from the OpenSSH release notes; distribution versions from Ubuntu package listings, sources.debian.org and Red Hat's May 2026 post on post-quantum SSH.

Is SSH post-quantum secure in 2026?

For key exchange, mostly yes, and earlier than most people think. OpenSSH's own post-quantum page explains the threat: an attacker records encrypted sessions today and decrypts them once a large enough quantum computer exists ("store now, decrypt later"). OpenSSH has defaulted to a post-quantum hybrid since 9.0 in April 2022 (sntrup761x25519-sha512), added the NIST-standardised ML-KEM hybrid in 9.9 and made it the default in 10.0. Signatures are a different story: breaking a host or user key later does not decrypt recorded traffic, so there is no retroactive risk and no rush.

Your exposure is old servers. OpenSSH 8.9 was the first release to put sntrup761 in the default key-exchange list, so anything older, such as Ubuntu 20.04 (8.2) or Debian 11 (8.4), cannot negotiate a post-quantum exchange, and a 10.1 or later client will warn on every connection. I reproduced both behaviours locally with an OpenSSH 10.3p1 client against a server restricted to curve25519-sha256:

# Which key exchange did this connection use? (OpenSSH 10.3p1 client)
ssh -v deploy@web-1 exit 2>&1 | grep 'kex: algorithm'
# debug1: kex: algorithm: mlkem768x25519-sha256

# Against a server without PQ kex, a 10.1+ client prints:
# ** 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

# Fleet audit: offer only PQ algorithms; non-PQ servers fail fast.
# Key exchange happens before authentication, so no login is needed.
for h in $(cat hosts.txt); do
  ssh -o BatchMode=yes -o ConnectTimeout=5 \
      -o KexAlgorithms=mlkem768x25519-sha256,sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com \
      "$h" true 2>&1 | grep -q 'no matching key exchange' && echo "NO-PQ $h"
done
# A non-PQ server answers: Unable to negotiate with ... no matching key
# exchange method found. Their offer: curve25519-sha256,...
Distribution OpenSSH shipped PQ key exchange with a current client Source
Ubuntu 22.04 LTS 8.9 Yes, sntrup761 ServerPilot version table
Ubuntu 24.04 LTS 9.6 Yes, sntrup761 ServerPilot version table
Ubuntu 26.04 LTS 10.2 Yes, ML-KEM default Ubuntu package listing
Debian 12 bookworm 9.2 Yes, sntrup761 sources.debian.org
Debian 13 trixie 10.0 Yes, ML-KEM default sources.debian.org
RHEL 10 9.9 Both; ML-KEM preferred from 10.1 Red Hat, May 2026
Ubuntu 20.04, Debian 11 8.2, 8.4 No, client warns OpenSSH 8.9 notes

A distribution's crypto policy or a hand-edited KexAlgorithms line can remove the hybrids, so trust the kex: algorithm line over the table. If you are planning the wider migration (TLS, VPNs, code signing), my post-quantum migration checklist covers the parts that are harder than SSH.

Which SSH key type should you use: Ed25519, RSA or FIDO2?

Ed25519 for software keys. It has been supported since OpenSSH 6.5 in January 2014 and became the ssh-keygen default in 9.5 (October 2023). RSA is still acceptable at 3072 bits or more because modern OpenSSH signs with rsa-sha2-256 or rsa-sha2-512; what died is the SHA-1 ssh-rsa signature, disabled by default in 8.8 (September 2021) after chosen-prefix SHA-1 collisions fell below US$50,000. DSA is gone: on my 10.3p1 client, ssh-keygen -t dsa now answers unknown key type dsa.

For humans, I would go further and use a FIDO2 security key. OpenSSH added the ecdsa-sk and ed25519-sk types in 8.2 (February 2020); the private key file on disk is only a handle, useless without the physical token, and by default every signature needs a touch. Generate with ssh-keygen -t ed25519-sk -O resident -O verify-required so the handle lives on the token and each use needs the PIN. The 10.5 and 10.6 releases kept polishing this path: the client now tries touch-free keys before PIN-protected ones, and ssh-add preserves the PIN requirement when loading resident keys.

What about the new ML-DSA hybrid keys that 10.6 enabled? I would wait. The scheme comes from an Internet-Draft (draft-miller-sshm-mldsa44-ed25519-composite-sigs, per the 10.4 notes), keys from the 10.4 experimental implementation must be regenerated, and the signature threat is not retroactive. Revisit when your distribution ships it and every other SSH implementation you depend on, including your Git host, accepts it.

SSH certificates vs authorized_keys: when should you switch?

An authorized_keys file is a per-user, per-host list of trusted keys with no expiry. Offboarding means editing every host, and nobody does. SSH certificates invert that: a certificate authority signs a user's public key with a list of principals (allowed usernames) and a validity window, and servers trust the CA through TrustedUserCAKeys. With eight-hour certificates, revocation is mostly a matter of not issuing tomorrow's. The mechanics are built into ssh-keygen; this is a real run on 10.3p1:

# CA signs a user key: identity, principals, 8-hour validity, no port forwarding
ssh-keygen -s user_ca -I "ajay@example.com" -n ajay,deploy -V +8h \
  -O no-port-forwarding id_ed25519.pub
# Signed user key id_ed25519-cert.pub: id "ajay@example.com" serial 0 for ajay,deploy
#   valid from 2026-10-24T15:50:00 to 2026-10-24T23:51:15

ssh-keygen -L -f id_ed25519-cert.pub
id_ed25519-cert.pub:
        Type: ssh-ed25519-cert-v01@openssh.com user certificate
        Public key: ED25519-CERT SHA256:nr4jtB/n/y7b6BMFO3gEj3zY9d2/4drarPGASlUfkkI
        Signing CA: ED25519 SHA256:C/ephfsFd0+KO98psuBd709qw4Nn2J4WtfGj26QmpI4 (using ssh-ed25519)
        Key ID: "ajay@example.com"
        Serial: 0
        Valid: from 2026-10-24T15:50:00 to 2026-10-24T23:51:15
        Principals:
                ajay
                deploy
        Critical Options: (none)
        Extensions:
                permit-X11-forwarding
                permit-agent-forwarding
                permit-pty
                permit-user-rc

Note what is missing from the extensions: permit-port-forwarding, removed by -O no-port-forwarding. On the server, TrustedUserCAKeys plus an AuthorizedPrincipalsFile per account means a new engineer needs a certificate, not a line in every authorized_keys file.

Two 2026 fixes are worth knowing if you already run a CA. The 10.3 change above means a mis-issued certificate with no principals is now useless rather than a skeleton key, and 10.6 fixed a daylight-saving bug in ssh-keygen date handling that could produce expiry times off by an hour. Managed options exist too: Google's OS Login accepts SSH certificate authentication and ties access to IAM roles. Certificates are also the cleanest way to give an AI agent or CI job shell access: short validity, a single principal, restrict or a forced command, and a key ID that shows up in every log line. I made the longer argument for that in non-human identity in 2026.

A hardened sshd_config for OpenSSH 10

This is the drop-in I would start from on a 9.9 or newer server. I validated it with sshd -t on OpenSSH 10.3p1; every directive is in the current sshd_config manual.

# /etc/ssh/sshd_config.d/00-hardening.conf
# Debian and Ubuntu read sshd_config.d/ near the top of sshd_config, and for
# each keyword the FIRST value read wins, so a low-numbered drop-in beats later lines.

PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
AuthenticationMethods publickey

# Ed25519, FIDO2 and RSA-SHA2 only: no SHA-1 ssh-rsa, no plain ECDSA
PubkeyAcceptedAlgorithms ssh-ed25519,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,sk-ssh-ed25519-cert-v01@openssh.com,sk-ecdsa-sha2-nistp256@openssh.com,rsa-sha2-512,rsa-sha2-256

# Post-quantum hybrids first; curve25519 kept for clients older than 8.9
KexAlgorithms mlkem768x25519-sha256,sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com,curve25519-sha256

# Short-lived user certificates, mapped to allowed principals per account
TrustedUserCAKeys /etc/ssh/user_ca.pub
AuthorizedPrincipalsFile /etc/ssh/principals/%u

# FIDO keys must prove a PIN; the man page says this has no effect on non-FIDO keys
PubkeyAuthOptions verify-required

MaxAuthTries 3
LoginGraceTime 30
AllowGroups ssh-users
# Remove on jump hosts: ProxyJump needs TCP forwarding
DisableForwarding yes
# Logs the key fingerprint on failures too
LogLevel VERBOSE

Apply it with sudo sshd -t && sudo systemctl reload ssh (the unit is sshd on RHEL-family systems), and keep your current session open until a second one logs in. Check the effective result with sudo sshd -T -C user=ajay,host=laptop,addr=198.51.100.7, which evaluates Match blocks for that connection. The "first value wins" rule is a common cause of lockouts, and my Permission denied (publickey) guide walks through debugging it. For basic brute-force noise you may not need fail2ban any more: since 9.8, PerSourcePenalties is on by default, and sshd -T on 10.3 shows the defaults as 5 seconds for a failed authentication, 90 for a crash, 5 for an invalid user, capped at 600.

Bastion host vs SSM Session Manager vs Tailscale SSH

The real best practice is that port 22 should not face the internet at all. Every major cloud now has an identity-aware path that removes the need for a self-managed jump host:

Option Inbound 22 from internet Who authenticates Documented cost Limits worth knowing
Self-managed bastion Yes, on the bastion sshd keys or certificates The instance and its public IPv4 You patch it, log it and watch it
AWS SSM Session Manager No inbound ports IAM No charge on EC2; $0.05 per session on hybrid nodes from 30 Sept 2026 Session logging is not available for SSH or port-forwarding sessions
EC2 Instance Connect Endpoint No public IP needed IAM, then your SSH key No additional cost; cross-AZ data billed 5 per Region, 1 per VPC, 20 concurrent; 1-hour connections
GCP IAP TCP forwarding + OS Login Only from 35.235.240.0/20 Google identity and IAM roles Not stated on the docs page Metadata SSH keys are ignored once OS Login is on
Tailscale SSH No public port Tailnet identity over WireGuard Per Tailscale plan Server side Linux and macOS tailscaled; check mode re-auth every 12 hours by default

Two details from the docs change how I use these. AWS states that Session Manager cannot record sessions that tunnel SSH through it, because it only sees encrypted bytes; if you need a transcript, use a native shell session or record on the host. And an EC2 Instance Connect Endpoint connection outlives the IAM credentials that opened it, up to the one-hour cap, so set maxTunnelDuration in the policy rather than trusting short credentials to end sessions.

What regreSSHion and the xz backdoor taught us about sshd

regreSSHion (CVE-2024-6387, July 2024) was a signal-handler race in sshd on glibc Linux that allowed unauthenticated remote code execution as root. It was a regression: a 2006 bug reintroduced in October 2020, present from 8.5p1 to 9.7p1. Qualys counted over 14 million potentially vulnerable internet-facing instances through Censys and Shodan. The OpenSSH 9.8 notes said exploitation took 6 to 8 hours of connections on average against 32-bit systems, and the stopgap for anyone who could not patch, LoginGraceTime 0, traded the RCE for an easy denial of service.

The xz backdoor, found by Andres Freund in March 2024, never touched OpenSSH's code. It sat in xz 5.6.0 and 5.6.1 and reached sshd because several distributions patch it to notify systemd, and libsystemd links liblzma. OpenSSH 9.8 responded with a standalone systemd notification implementation that does not depend on libsystemd.

Neither lesson is about cryptography. Both are about knowing which sshd build runs on every host, what it links, and how fast a fix reaches it. That is configuration management, inventory and unattended patching, the boring platform work that matters more now that OpenSSH says AI tools are surfacing bugs faster than before. Keep sshd_config in Git, render it from one template, test it with sshd -t in CI, and alert on any host whose ssh -V lags the distribution's current security update.

FAQ

Should I change the SSH port from 22?

It reduces log noise from scanners, but it is not a security control. Disabling passwords, using keys or certificates, and keeping 22 off the internet behind SSM, IAP or a tailnet does far more. On Ubuntu with socket activation, a port change also needs systemctl daemon-reload (Launchpad bug 2069041).

Is Ed25519 more secure than RSA for SSH?

Both are fine at sensible sizes; Ed25519 is faster, has tiny keys, and avoids RSA's padding and key-size choices, which is why it has been the ssh-keygen default since OpenSSH 9.5. Use RSA of 3072 bits or more only when a legacy system cannot take Ed25519, and never with SHA-1 ssh-rsa signatures.

Is OpenSSH post-quantum by default?

Yes. Key exchange has been a post-quantum hybrid by default since OpenSSH 9.0, and since 10.0 the default is mlkem768x25519-sha256. Run ssh -v host exit 2>&1 | grep 'kex: algorithm' to confirm a given connection; servers older than 8.9 will negotiate a classical exchange and trigger a warning on 10.1+ clients.

Do I still need a bastion host in 2026?

Usually not. AWS Session Manager and EC2 Instance Connect Endpoint, Google IAP with OS Login, Azure Bastion and Tailscale SSH all give identity-checked access without exposing port 22. Keep a bastion only where you need plain OpenSSH semantics across networks you control, and treat it as the most patched host you own.

This quarter I would do five things: run the PQ-only audit loop above and upgrade anything that fails it; move human logins to FIDO2 ed25519-sk keys with PIN verification; issue short-lived certificates for people, CI and agents instead of adding lines to authorized_keys; ship the drop-in above through configuration management with an sshd -t gate; and close port 22 from the internet in favour of whichever identity-aware proxy your cloud already provides. None of it is new technology, which is the point.

Related: Post-Quantum Migration in 2026, Secrets Sprawl 2026 and Workload Identity Federation Explained.

I'm Ajay Kumar — I build and operate PandaStack, an open-source Firecracker microVM cloud for AI agents, and write about the infrastructure and tooling around it.

Need this kind of infrastructure work? See what I do or email hello@ajayk.sh.


Related