How to harden sshd_config: a checklist you can test

Put your settings in a drop-in file such as /etc/ssh/sshd_config.d/10-hardening.conf: PermitRootLogin no, PasswordAuthentication no, KbdInteractiveAuthentication no, AllowUsers with your login, and lower MaxAuthTries and LoginGraceTime. Check with sudo sshd -t, reload sshd, and test a new login while your current session stays open.

Updated . Config, sshd -t, sshd -T, sshd -T -C and every refusal shown here were run on OpenSSH 10.0p1 on Debian 13 in a throwaway container, with an OpenSSH 10.3 client on macOS.

Keep a session open while you test

Reloading sshd does not end existing connections. In our test, a session opened before the reload kept running and finished normally after it. So leave one root-capable session open, and do all testing from a second terminal. If a setting locks you out, you fix it from the first one.

Before you set PasswordAuthentication no, make sure a key login works. See how to disable SSH password login.

Use a drop-in file, and know that the first value wins

For most settings, sshd keeps the first value it reads. Debian and Ubuntu have Include /etc/ssh/sshd_config.d/*.conf at the top of sshd_config, so a drop-in beats a line further down the main file. On our test system the main file had PasswordAuthentication yes on line 125, and the drop-in with no won.

Files in that folder are read in name order, and some cloud images add their own, so a low number such as 10- is a safe choice. If your system has no Include line, edit sshd_config directly and put the settings before any Match block.

The settings

# /etc/ssh/sshd_config.d/10-hardening.conf
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
AllowUsers deploy
MaxAuthTries 3
LoginGraceTime 30
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding no
SettingUpstream defaultWhat the change does
PermitRootLogin noprohibit-passwordRoot cannot log in at all, even with a key. People log in as themselves and use sudo, which leaves a record of who did what.
PasswordAuthentication noyesRemoves password guessing as an attack.
KbdInteractiveAuthentication noyesCloses the other route to a password prompt. Leave it on if you use one-time codes through PAM.
AllowUsers deployNot set: any account can tryOnly the listed accounts can log in. A typo here locks everyone out, which is why the first session stays open.
MaxAuthTries 36Fewer attempts per connection. Each key offered by a client counts, so a client with many keys can run out. See Too many authentication failures.
LoginGraceTime 30120 secondsUnauthenticated connections are dropped sooner.
X11Forwarding nono, but Debian's file sets yesTurns off a feature most servers never use.
AllowAgentForwarding noyesSee the risks of agent forwarding.
AllowTcpForwarding noyesStops SSH tunnels through this server, and its use as a jump host. Do not set this if you use either.

The manual is clear about one limit: disabling TCP forwarding does not improve security unless users are also denied a shell, because anyone with a shell can run their own forwarder. Treat it as cleanup for accounts that have a shell, and as a real restriction only for accounts that do not.

Leave MaxStartups and PerSourcePenalties at their defaults unless you have a reason. They already slow down floods of connections on current versions.

Check before you reload

sshd -t checks the syntax and the host keys. It prints nothing when all is well. A typo gives an error and a non-zero exit code:

$ sudo sshd -t
/etc/ssh/sshd_config.d/99-typo.conf: line 1: Bad configuration option: PermitRootLogn
/etc/ssh/sshd_config.d/99-typo.conf: terminating, 1 bad configuration options

sshd -T prints the values sshd will really use, after all files are combined. Names appear in lowercase:

sudo sshd -T | grep -iE '^(permitrootlogin|passwordauthentication|kbdinteractiveauthentication|allowusers|maxauthtries|logingracetime|x11forwarding|allowagentforwarding|allowtcpforwarding) '

If you use Match blocks, add -C to see the result for one particular connection:

sudo sshd -T -C user=deploy,host=example.com,addr=203.0.113.10 | grep -i allowtcpforwarding

sshd -T reads the files on disk, not what the running daemon loaded. It shows what you will get after the reload.

Reload and test from a second terminal

# Debian, Ubuntu
sudo systemctl reload ssh
# Red Hat family
sudo systemctl reload sshd

Then try each of these. The first must succeed and the others must fail:

ssh deploy@server 'echo ok'
ssh -o PubkeyAuthentication=no -o PreferredAuthentications=password deploy@server
ssh root@server
ssh -N -L 8022:127.0.0.1:22 deploy@server

The last command does not fail at login. It connects, and the forward is refused when you use the local port. The server log then shows a line like this:

refused local port forward: originator 127.0.0.1 port 63434, target 127.0.0.1 port 22

A refused root login appears in the log as User root from 203.0.113.10 not allowed because not listed in AllowUsers.

Modern algorithms: only if you need to restrict them

Current OpenSSH defaults are good. On OpenSSH 10.0 the default key exchange list starts with the hybrid post-quantum mlkem768x25519-sha256. Setting your own lists means you must maintain them, so do it when an audit requires it or when your system is old. This set keeps only modern choices:

KexAlgorithms mlkem768x25519-sha256,sntrup761x25519-sha512,curve25519-sha256,curve25519-sha256@libssh.org
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com

Older OpenSSH versions do not know some of these names. Run ssh -Q kex, ssh -Q cipher and ssh -Q mac on the server and remove what is not listed. Check what a client really negotiates:

ssh -v deploy@server true 2>&1 | grep -E 'kex: algorithm|server->client cipher'

Old clients and devices will fail with this message, which has its own guide:

Unable to negotiate with 203.0.113.10 port 22: no matching key exchange method found.

This list also removes the NIST curve and Diffie-Hellman exchanges, so test every device and script that connects to the server before you keep it.

What this does not cover

These settings shrink what can be tried against sshd. They do not patch OpenSSH, so keep it updated. They also do not limit what a logged-in user can do. For repeated attacks in your logs, see fail2ban for SSH.

To see who is trying, see how to check SSH login attempts.

Questions

Does reloading sshd close my current SSH session?

No. Existing sessions keep running after a reload. Only new logins use the new settings, which is why you test from a second terminal.

Where should I put my own sshd settings?

In a file in /etc/ssh/sshd_config.d/ with a low number, such as 10-hardening.conf. sshd keeps the first value it reads, so a drop-in read early wins.

How do I check sshd_config for errors?

Run sudo sshd -t. It prints nothing when the configuration is valid, and an error with a non-zero exit code when it is not.

Sources

More in Securing a server

All guides