SSH hardening without locking yourself out: keys, root, fail2ban

Updated November 2026

The goal is simple: only people holding a private key can log in (SSH key authentication), root cannot log in with a password, and only named accounts are allowed. The risk is locking yourself out, so do it in a fixed order and keep one working session open until a fresh login succeeds.

The safe order

1. Install your keyssh-copy-id 2. Test key loginand sudo 3. Write a drop-insshd_config.d 4. sudo sshd -tno output = OK 5. Reload sshdkeep this session open 6. New terminal loginworks: close the old one Login fails?Fix it from the old session
Never close your only working session until a brand-new login has succeeded.

Before you start, make sure you have a way in that does not depend on SSH: the hosting provider's web console, IPMI, or physical access. If everything goes wrong, see Locked out of SSH.

Create and install a key

On your own computer, not the server, create an Ed25519 key. It is the default type in current OpenSSH and is short and fast. Protect it with a passphrase.

# on your workstation
ssh-keygen -t ed25519 -a 100 -C "alice@laptop"

# copy the public key to your account on the server
ssh-copy-id -i ~/.ssh/id_ed25519.pub alice@server.example.com

# test: this must log in without asking for the account password
ssh -o PreferredAuthentications=publickey alice@server.example.com
sudo -v   # on the server: confirm you can still use sudo

-a sets the number of key-derivation rounds that protect the passphrase; more rounds make a stolen key file slower to crack. Check old keys too: OpenSSH 10.0 removed DSA support entirely, so ssh-dss keys in authorized_keys no longer work and should be deleted.

Where to put settings

Don't edit the main /etc/ssh/sshd_config; put your settings in a drop-in file in /etc/ssh/sshd_config.d/. Package updates leave it alone, and it is easy to review.

One rule matters more than any other: for most keywords, sshd uses the first value it reads and ignores later ones (sshd_config(5): "the first obtained value will be used"). That decides which file wins:

So always check the effective result, not the file you wrote. sshd -T prints the configuration sshd will actually use:

sudo sshd -T | grep -Ei '^(permitrootlogin|passwordauthentication|kbdinteractiveauthentication|pubkeyauthentication|allowusers|allowgroups)'

The settings

Create /etc/ssh/sshd_config.d/10-hardening.conf (use 40-hardening.conf on RHEL):

PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
AllowGroups sshusers
MaxAuthTries 3
PasswordAuthentication no
Turns off password logins. The upstream default is yes.
KbdInteractiveAuthentication no
Keyboard-interactive login is the other way a password prompt reaches PAM. Upstream defaults it to yes (Debian's packaged config sets no). Leave it on only if you use it for MFA, below. ChallengeResponseAuthentication is a deprecated alias.
PermitRootLogin
The default, prohibit-password, allows root only with a key. Use no once every admin has a named account with sudo; it also records who logged in. without-password is a deprecated alias of prohibit-password.
AllowGroups / AllowUsers
Only listed groups or users may log in at all. Create the group and add yourself first: sudo groupadd sshusers, sudo usermod -aG sshusers alice. sshd checks DenyUsers, AllowUsers, DenyGroups, then AllowGroups, and a user must pass all that are set. AllowUsers also accepts user@address patterns, such as alice@203.0.113.*.
MaxAuthTries 3
Fewer attempts per connection (default 6). If you have many keys loaded in your agent, pick the right one with -i or IdentitiesOnly yes, or you may hit the limit.

Test and reload

# 1. syntax and key check: no output means OK
sudo sshd -t

# 2. confirm the effective values (see the grep above)
sudo sshd -T | grep -Ei '^(passwordauthentication|permitrootlogin|allowgroups)'

# 3. reload (existing sessions stay connected)
sudo systemctl reload ssh     # Debian, Ubuntu
sudo systemctl reload sshd    # RHEL family, Fedora, SUSE

Now, without closing your current session, open a new terminal and log in. Also confirm passwords are refused:

ssh -o PubkeyAuthentication=no -o PreferredAuthentications=password alice@server.example.com
# expected: Permission denied (publickey).

If the new login fails, fix the drop-in from the old session, run sshd -t and reload again.

Ubuntu and changing the port. Since Ubuntu 22.10, sshd is started by systemd socket activation (ssh.socket). On Ubuntu 24.04 and later, the Port and ListenAddress values are read from the sshd configuration by a systemd generator, so after changing them run sudo systemctl daemon-reload and sudo systemctl restart ssh.socket, and open the new port in the firewall first. Other settings only need the reload above. Moving SSH off port 22 cuts log noise but is not a security control on its own.

Limit where logins come from

If admins connect from fixed addresses or a VPN, allow port 22 only from those in the firewall. This stops almost all scanning and leaves fail2ban with little to do. See Firewall basics.

Brute-force protection

With passwords off, password guessing cannot succeed, but the noise fills logs and uses CPU. Two layers help:

[sshd]
enabled  = true
backend  = systemd
maxretry = 5
findtime = 10m
bantime  = 1h
ignoreip = 127.0.0.1/8 ::1 203.0.113.10
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd
sudo fail2ban-client set sshd unbanip 203.0.113.10   # if you banned yourself

Put your own fixed address in ignoreip. The systemd backend reads the journal, which matters on systems without /var/log/auth.log. If you face an active attack, see Brute-force attacks.

MFA options

Keep OpenSSH updated

sshd runs as root and faces the network, so its updates are urgent. In July 2024, OpenSSH 9.8 fixed "regreSSHion" (CVE-2024-6387), a race condition that could allow remote code execution as root in Portable OpenSSH 8.5p1 to 9.7p1. As of November 2026 the newest upstream release is OpenSSH 10.6 (October 2026), but distributions backport fixes into older version numbers, so judge by your vendor's advisory, not the version string. Install updates with the normal package tools, then restart sshd so the listener runs the new code (package scripts usually do this). See Check installed versions and Security advisories.

Sources