Hardening SSH on Debian 13 with OpenSSH 10

9 min read

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-dss key, replace it with Ed25519 before you upgrade.
  • Post-quantum key exchange is the default. OpenSSH 10.0 uses mlkem768x25519-sha256 by default. It is a hybrid of ML-KEM and X25519, so it is at least as strong as curve25519-sha256.
  • Finite-field Diffie-Hellman is off by default on the server. The diffie-hellman-group* methods are no longer in the server's default KexAlgorithms.
  • Authentication runs in a separate binary. The user-authentication code moved out of sshd-session into a new sshd-auth binary. The code that handles unauthenticated clients now runs in its own address space.
  • ~/.pam_environment is 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-gssapi and openssh-client-gssapi packages 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:

OptionUpstream defaultDebian shipped config
Includenone/etc/ssh/sshd_config.d/*.conf at the top
UsePAMnoyes
KbdInteractiveAuthenticationyesno
X11Forwardingnoyes
PasswordAuthenticationyesyes (not changed)
PermitRootLoginprohibit-passwordprohibit-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:

  1. Make sure key login works for the account you'll use, and that it can sudo.
  2. Keep an existing root shell open while you test.
  3. 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.net

If 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 admin

Then 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 VERBOSE

Here'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::/48

Put 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=yes did not override PermitTunnel=yes as documented. That's why the drop-in sets PermitTunnel no and every other forwarding option explicitly, and doesn't rely on DisableForwarding alone.
  • CVE-2026-73283: before 10.5, the restrict keyword in authorized_keys did not cover tunnel forwarding. A global PermitTunnel no covers this too.
  • CVE-2026-60000: before 10.4, MaxAuthTries was not enforced for GSSAPI, which allows a pre-authentication denial of service. Keep GSSAPIAuthentication no unless you really use Kerberos.
  • CVE-2026-60001: sshd did 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-sftp silently dropped arguments after the ninth one. If you pass options such as -R, -P or -u to internal-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.gz

Exceptions 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 no

Remember 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 -f

From 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-sha256

Clients 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 an AllowGroups allowlist.
  • Replace any DSA keys before upgrading. Use Ed25519, or ed25519-sk with a hardware token.
  • Keep the post-quantum KEX default, and consider dropping the NIST curves.
  • PerSourcePenalties is on by default. Exempt your monitoring hosts, and group IPv6 addresses if needed.
  • Set PermitTunnel no and GSSAPIAuthentication no explicitly, because trixie's 10.0 still has the DisableForwarding/restrict and GSSAPI bugs.
  • Check with sshd -t and sshd -T -C, reload ssh, and test from a second session.
  • Check the Debian security tracker for openssh regularly. Several fixes had not been backported to trixie when this was written.

Sources

Comments