SSH is the single most attacked service on any internet-facing server. A default install with password authentication sees thousands of login attempts a day. Everything below is cheap and none of it breaks normal use.
Keys only, and use Ed25519
# On your workstation, not the server
ssh-keygen -t ed25519 -a 100 -C "deploy@$(hostname)"
ssh-copy-id -i ~/.ssh/id_ed25519.pub deploy@YOUR_IP
Ed25519 is faster and shorter than RSA at equivalent strength. -a 100 increases the KDF rounds on the private key, making an offline attack against a stolen key file considerably more expensive.
A sane sshd_config
# /etc/ssh/sshd_config
Port 22
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
MaxAuthTries 3
MaxSessions 5
LoginGraceTime 20
AllowUsers deploy
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding yes
ClientAliveInterval 300
ClientAliveCountMax 2
# Modern algorithms only, hybrid post-quantum key agreement first
KexAlgorithms mlkem768x25519-sha256,[email protected],curve25519-sha256,[email protected]
Ciphers [email protected],[email protected]
MACs [email protected],[email protected]
sshd -t && systemctl reload sshd
Always sshd -t first. Always keep a second session open until you have confirmed the new config works.
Two things about that KexAlgorithms line. It names algorithms your sshd may not know — mlkem768x25519-sha256 needs OpenSSH 9.9 or newer and [email protected] needs 8.5 or newer, and an unrecognised name makes sshd refuse to start, which is what sshd -t is for. And it is a list with an expiry date: the hybrid post-quantum schemes here became upstream defaults in 9.0 and again in 10.0, and the next one will arrive the same way. Put a reminder in your calendar to re-read this line once a year, or delete it and take whatever your OpenSSH ships with.
On changing the port
Moving SSH off 22 eliminates essentially all automated noise in your logs. It does not stop a targeted attacker, who will portscan you in seconds. It is worth doing for log hygiene and nothing else — do not treat it as a security control.
fail2ban
apt -y install fail2ban
cat > /etc/fail2ban/jail.local <<'EOF'
[sshd]
enabled = true
maxretry = 3
findtime = 600
bantime = 86400
EOF
systemctl enable --now fail2ban
With password authentication disabled fail2ban is mostly about log volume, but it also drops the cost of a key-guessing flood.
Two-factor for the paranoid
apt -y install libpam-google-authenticator
google-authenticator -t -d -f -r 3 -R 30 -W
# /etc/ssh/sshd_config
AuthenticationMethods publickey,keyboard-interactive
KbdInteractiveAuthentication yes
# /etc/pam.d/sshd — add at the top
auth required pam_google_authenticator.so nullok
This requires a key and a TOTP code. Store the emergency scratch codes somewhere that is not the server.
Hardware keys
OpenSSH 8.2+ supports FIDO2 tokens natively:
ssh-keygen -t ed25519-sk -O resident -O verify-required
The private key cannot be exfiltrated from the token, which defeats the most common realistic compromise: a stolen laptop or a compromised workstation.
Verify your work
ssh -o PasswordAuthentication=yes -o PubkeyAuthentication=no deploy@YOUR_IP
# Expected: Permission denied (publickey)
ssh root@YOUR_IP
# Expected: Permission denied (publickey)