OpenSSH hardening checklist

2025-07-22 — ssh, security

Settings I apply to every new server. Some of these are already the modern defaults but I set them explicitly so the config is self-documenting. All of this goes in /etc/ssh/sshd_config (or a file in /etc/ssh/sshd_config.d/).

Always test in a second terminal before restarting sshd. Always.

Key-based authentication setup

Generate a key pair locally if you don't have one. Ed25519 is preferred — smaller keys, faster, more modern than RSA:

ssh-keygen -t ed25519 -C "$(hostname)-$(date +%Y%m%d)"

Copy the public key to the server:

ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server

Or manually append it to ~/.ssh/authorized_keys on the server. Verify you can log in with the key before doing anything else.

sshd_config settings

Disable password and root login

PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes

KbdInteractiveAuthentication no covers keyboard-interactive auth, which some PAM configs use as a backdoor around PasswordAuthentication no. Set both to be safe.

Limit authentication attempts

MaxAuthTries 3
MaxSessions 5

MaxAuthTries 3 reduces the window for automated credential stuffing. The default is 6, which is more than enough for a typo but also more than enough for a fast brute-force attempt per connection.

Restrict what users can do

X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding no
PermitTunnel no

Disable features you're not using. If you need agent forwarding for a specific workflow, enable it consciously rather than leaving it on by default.

Connection keepalive

ClientAliveInterval 120
ClientAliveCountMax 3

Server sends a keepalive packet every 120 seconds. After 3 missed responses (6 minutes of silence) it drops the connection. Prevents ghost sessions from accumulating.

Miscellaneous

LoginGraceTime 30
PrintLastLog yes
Banner none

LoginGraceTime 30 — unauthenticated connections are dropped after 30 seconds. Default is 120, which is a long time to leave a connection hanging. Banner none — no need to announce what software you're running.

Restricting access by user

If there's only one account that needs SSH access, lock it down explicitly:

AllowUsers youruser

This causes an implicit DenyUsers * for everyone else. More explicit than relying on key auth alone.

Verify config before restarting

# Check config for syntax errors
sshd -t

# If clean, reload (less disruptive than restart — doesn't kill existing sessions)
systemctl reload ssh
Do this from your existing session. Open a second terminal and try to log in before closing the first one. If you made a mistake and locked yourself out, you still have the first session to fix it.

Checking active connections

# Who is currently connected
who
w

# Active SSH sessions via ss
ss -tnp | grep :22

# Recent logins
last | head -20

fail2ban for brute force

Even with password auth disabled, automated SSH scanners will keep hammering port 22. fail2ban watches the auth log and temporarily bans IPs that fail too many times. On Debian with nftables:

apt install fail2ban

# /etc/fail2ban/jail.local
[DEFAULT]
bantime  = 1h
findtime = 10m
maxretry = 5
banaction = nftables
banaction_allports = nftables[type=allports]
bantime.increment = true
bantime.maxtime   = 1w

The sshd jail is enabled by default on Debian. See the fail2ban reference page for status and management commands.