Almost every Debian box I run has one service exposed to the internet: sshd. Debian 13 (trixie) ships OpenSSH 10.0, and it is a better starting point than Bookworm's 9.2. DSA is gone, post-quantum key exchange is on by default, and sshd now slows down abusive clients by itself. The Debian defaults still allow password logins and X11 forwarding, though, and some OpenSSH bugs that were fixed upstream have not been backported to trixie yet. This post covers hardening SSH on Debian 13: what changed, one drop-in config file, how to check it, and which open issues to configure around.
Everything below was checked against openssh-server 1:10.0p1-7+deb13u4, the version in trixie at the time of writing (September 2026).
What changed in OpenSSH on Debian 13
Before you touch any config, you should know about these changes:
- DSA keys are gone. The trixie release notes say DSA keys no longer work, even if you try to re-enable them in the config. The OpenSSH 10.0 release notes confirm the algorithm was removed completely. If an old appliance or script still uses an
ssh-dsskey, replace it with Ed25519 before you upgrade. - Post-quantum key exchange is the default. OpenSSH 10.0 uses
mlkem768x25519-sha256by default. It is a hybrid of ML-KEM and X25519, so it is at least as strong ascurve25519-sha256. - Finite-field Diffie-Hellman is off by default on the server. The
diffie-hellman-group*methods are no longer in the server's defaultKexAlgorithms. - Authentication runs in a separate binary. The user-authentication code moved out of
sshd-sessioninto a newsshd-authbinary. The code that handles unauthenticated clients now runs in its own address space. ~/.pam_environmentis no longer read. Debian dropped this by default because of its history of security problems.- GSS-API is being split out. The release notes say the main package will stop supporting GSS-API in forky, the next Debian release. If you use Kerberos logins, the separate
openssh-server-gssapiandopenssh-client-gssapipackages are the way forward.
Know the Debian defaults before you change them
The sshd_config(5) man page in trixie lists where Debian's defaults differ from upstream. The ones that matter for hardening:
| Option | Upstream default | Debian shipped config |
|---|---|---|
Include | none | /etc/ssh/sshd_config.d/*.conf at the top |
UsePAM | no | yes |
KbdInteractiveAuthentication | yes | no |
X11Forwarding | no | yes |
PasswordAuthentication | yes | yes (not changed) |
PermitRootLogin | prohibit-password | prohibit-password (not changed) |
With sshd, the first value it reads for an option is the one it uses. Debian puts the Include line at the top of sshd_config, so settings in /etc/ssh/sshd_config.d/ override the main file. Put your hardening there and leave the packaged sshd_config alone. Upgrades then won't ask you to merge conffile changes.
Before you lock yourself out
Do these first:
- Make sure key login works for the account you'll use, and that it can
sudo. - Keep an existing root shell open while you test.
- Have out-of-band access ready: a provider console, IPMI, or physical access.
If your users still have old RSA keys, create Ed25519 keys on the client side:
ssh-keygen -t ed25519 -a 100 -C "pawel@laptop-2026"
ssh-copy-id -i ~/.ssh/id_ed25519.pub admin@server.example.netIf you have a FIDO2 token, use -t ed25519-sk. Then a stolen private key file is useless without the hardware.
A hardening drop-in for sshd on Debian 13
Create a group for SSH users and add your admin account to it:
groupadd --system sshusers
usermod -aG sshusers adminThen create /etc/ssh/sshd_config.d/00-hardening.conf:
# /etc/ssh/sshd_config.d/00-hardening.conf
# Read before /etc/ssh/sshd_config; first value wins.
# --- Who can log in, and how ---
PermitRootLogin no
AllowGroups sshusers
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitEmptyPasswords no
AuthenticationMethods publickey
GSSAPIAuthentication no
HostbasedAuthentication no
# --- Limit brute force and slow connections ---
LoginGraceTime 30
MaxAuthTries 3
MaxSessions 4
MaxStartups 10:30:60
# --- Forwarding: off unless someone needs it ---
DisableForwarding yes
PermitTunnel no
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding no
AllowStreamLocalForwarding no
PermitUserEnvironment no
# --- Crypto ---
HostKey /etc/ssh/ssh_host_ed25519_key
HostKey /etc/ssh/ssh_host_rsa_key
KexAlgorithms mlkem768x25519-sha256,sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org
RequiredRSASize 3072
# --- Idle sessions and logging ---
ClientAliveInterval 300
ClientAliveCountMax 2
LogLevel VERBOSEHere's why each group of settings is there.
Authentication
PasswordAuthentication no together with AuthenticationMethods publickey means only keys are accepted. Debian already ships with KbdInteractiveAuthentication no. Setting it here as well protects you if the main file is ever edited, because with UsePAM yes, keyboard-interactive would let PAM ask for a password again. AllowGroups is your allowlist: service accounts created by packages can never log in over SSH, even if someone gives them a shell and a password.
If you want a second factor on top of the key, you can chain methods, for example AuthenticationMethods publickey,keyboard-interactive with a TOTP PAM module. Test that on a second port before you roll it out.
Rate limiting: PerSourcePenalties
I didn't set anything for rate limiting, on purpose. PerSourcePenalties is on by default in this version. The trixie man page lists these default penalties per source address: 5s for a failed authentication, 1s for a connection that never authenticates, 10s for exceeding LoginGraceTime, and 90s if a connection makes sshd crash. Penalties add up to a maximum of 10 minutes. PerSourceNetBlockSize defaults to 32:128, which means every IPv4 and IPv6 address is tracked separately. If you get attacked from whole IPv6 networks, you can group them:
PerSourceNetBlockSize 32:64
PerSourcePenaltyExemptList 192.0.2.10,2001:db8:100::/48Put your monitoring and jump hosts in the exempt list. Otherwise a broken health check can lock out your own infrastructure. Fail2ban still helps if you want bans across several services or at the firewall, but you no longer need it just to stop simple password guessing.
Crypto
The upstream default KexAlgorithms list in trixie still ends with the NIST ecdh-sha2-nistp* curves. The list above keeps only the post-quantum hybrids and X25519. Every OpenSSH client from the last several years supports curve25519-sha256. Old network gear and some Java SSH libraries might not, so test those before you drop the NIST curves. I don't set Ciphers or MACs: the OpenSSH 10.0 defaults are fine, and pinning them just means you have to update the list later.
RequiredRSASize 3072 rejects RSA user keys and host keys smaller than 3072 bits. The built-in minimum is 1024, and you can only raise it. If you don't need RSA at all, remove the RSA HostKey line.
Configure around open issues in trixie
OpenSSH has had several security releases since 10.0: 10.3 in April 2026, 10.4 in July 2026 and 10.5 in August 2026. When I checked, the Debian security tracker listed several of those fixes as still vulnerable in trixie. Forky and sid are fixed with 10.5p1. Debian rates most of them minor (no-dsa or postponed). Several of them are about hardening options that don't do what they say:
- CVE-2026-59999: before 10.4,
DisableForwarding=yesdid not overridePermitTunnel=yesas documented. That's why the drop-in setsPermitTunnel noand every other forwarding option explicitly, and doesn't rely onDisableForwardingalone. - CVE-2026-73283: before 10.5, the
restrictkeyword inauthorized_keysdid not cover tunnel forwarding. A globalPermitTunnel nocovers this too. - CVE-2026-60000: before 10.4,
MaxAuthTrieswas not enforced for GSSAPI, which allows a pre-authentication denial of service. KeepGSSAPIAuthentication nounless you really use Kerberos. - CVE-2026-60001:
sshddid not always enforce the minimum authentication delay. Key-only authentication makes this mostly irrelevant, which is another reason to turn passwords off. - CVE-2026-59997:
internal-sftpsilently dropped arguments after the ninth one. If you pass options such as-R,-Por-utointernal-sftp, keep the command line short and check that each option takes effect.
The tracker also lists client-side and ssh-agent bugs, so the laptops you connect from need patching too. Check the current status yourself before you rely on this list. It changes as point releases land:
apt-get update && apt list --upgradable 2>/dev/null | grep -i openssh
zless /usr/share/doc/openssh-server/changelog.Debian.gzExceptions with Match blocks
If one group needs SFTP only, put the exception in a separate file that is read later, for example /etc/ssh/sshd_config.d/50-sftp.conf:
Match Group sftponly
ForceCommand internal-sftp -u 0027
ChrootDirectory /srv/sftp/%u
DisableForwarding yes
PermitTunnel noRemember to add sftponly to AllowGroups. The chroot directory must be owned by root and must not be writable by the user. Test how Match blocks resolve instead of guessing. The next section shows how.
Validate, reload, verify
First check the syntax. Then look at the effective config for a specific connection:
sshd -t
sshd -T | grep -Ei 'passwordauth|permitroot|kexalgorithms|permittunnel|persourcepen'
sshd -T -C user=alice,host=client.example.net,addr=198.51.100.7 | grep -Ei 'forcecommand|chroot'On Debian the service is called ssh, not sshd. Reloading keeps existing sessions open:
systemctl reload ssh
journalctl -u ssh -fFrom a second terminal, confirm that password login is refused and check which key exchange was used:
ssh -o PubkeyAuthentication=no admin@server.example.net
# expect: Permission denied (publickey).
ssh -v admin@server.example.net 2>&1 | grep 'kex: algorithm'
# expect: debug1: kex: algorithm: mlkem768x25519-sha256Clients running OpenSSH 10.1 or newer warn when a connection uses a key exchange that isn't post-quantum. You can turn that off with WarnWeakCrypto, but if you see the warning against this server, check your config.
With LogLevel VERBOSE, the journal shows the fingerprint of the key used for each login. That's what you need when you have to find out which key was used from where.
Network layer
Also restrict who can reach port 22. Moving to another port only reduces log noise; it doesn't protect anything. Real options: allow SSH only from a VPN or bastion, or at least rate-limit new connections with nftables:
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
ct state established,related accept
iif lo accept
tcp dport 22 ct state new limit rate 10/minute burst 20 packets accept
}
}If you administer the box over IPv6, add ICMPv6 rules before you apply this ruleset.
Takeaways
- Put hardening in
/etc/ssh/sshd_config.d/00-hardening.conf. The first value read wins, and Debian includes the drop-in directory first. - Use keys only (
PasswordAuthentication no,AuthenticationMethods publickey), no root login, and anAllowGroupsallowlist. - Replace any DSA keys before upgrading. Use Ed25519, or
ed25519-skwith a hardware token. - Keep the post-quantum KEX default, and consider dropping the NIST curves.
PerSourcePenaltiesis on by default. Exempt your monitoring hosts, and group IPv6 addresses if needed.- Set
PermitTunnel noandGSSAPIAuthentication noexplicitly, because trixie's 10.0 still has theDisableForwarding/restrictand GSSAPI bugs. - Check with
sshd -tandsshd -T -C, reloadssh, and test from a second session. - Check the Debian security tracker for
opensshregularly. Several fixes had not been backported to trixie when this was written.
Sources
- Debian 13 (trixie) release notes: Issues to be aware of
- OpenSSH 10.0 release notes
- OpenSSH 10.1 release notes
- OpenSSH release notes (10.2-10.5)
- sshd_config(5), Debian trixie manpage
- Debian package: openssh-server in trixie
- Debian security tracker: openssh
- Debian security tracker: CVE-2026-59999
- Debian security tracker: CVE-2026-60000
- Debian security tracker: CVE-2026-60001
Comments