SSH Hardening: Key Auth and Login Defense
Complete SSH server hardening: Ed25519 keys instead of passwords, disable root login, port move, fail2ban, sshd_config checks and troubleshooting.
Scope
SSH hardening applies to every Linux host managed over SSH: cloud VMs, bare metal, container hosts. Treat this as baseline if the box is on the public internet, still allows password login, or already shows brute-force logs. Validate on a test host before production so you do not lock out remote access.
Prerequisites
- Linux server with root or sudo, SSH working (OpenSSH 7.x+)
- Local SSH key pair already generated (
ssh-keygen -t ed25519); public key ready to upload - Backup access (cloud vendor VNC/console) in case a bad config cuts SSH
How it works
Core idea: shrink the attack surface and tighten auth. Default sshd_config allows root password login with unlimited retries; attackers dictionary-blast that. Four layers cut risk: 1) replace passwords with Ed25519 key auth — high strength, not dictionary-guessable; 2) disable PermitRootLogin and PasswordAuthentication — remove the root blast door; 3) AllowUsers whitelist plus MaxAuthTries cap; 4) fail2ban + firewall auto-ban abusive source IPs. Defense in depth.
Steps
1. Generate and deploy key pair
Generate an Ed25519 key locally (stronger and faster than RSA) and append the public key to the server authorized_keys:
# run on local machine
ssh-keygen -t ed25519 -C "admin@example.com" -f ~/.ssh/id_ed25519
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server_ip
2. Set core sshd_config parameters
Edit /etc/ssh/sshd_config. Confirm each parameter:
# deny root login; use a normal user + sudo
PermitRootLogin no
# key auth only; disable passwords
PasswordAuthentication no
# max key auth attempts
MaxAuthTries 3
# login timeout 60 seconds
LoginGraceTime 60
# login user whitelist (replace as needed)
AllowUsers admin deploy
# listen only on required address (can switch to internal)
ListenAddress 0.0.0.0
Save, syntax-check, then reload:
sudo sshd -t && sudo systemctl reload sshd
If sshd -t errors, the config is wrong. Do not reload. Fix it immediately.
3. Change default port and restrict source
Move SSH off 22 to a high port (e.g. 2222) to cut scanner hits. Update the firewall after sshd_config:
Port 2222
# after change
sudo systemctl restart sshd
# iptables allow new port
sudo iptables -A INPUT -p tcp --dport 2222 -s your_office_ip -j ACCEPT
4. Wire fail2ban and firewall
Key auth already blocks password sprays. Still need a backstop for application-layer abuse (failed auth retries). Install fail2ban and enable the sshd jail:
sudo apt install fail2ban
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
# enable in jail.local
[sshd]
enabled = true
maxretry = 3
bantime = 3600
sudo systemctl restart fail2ban
Verify
# 1. verify port and auth method (open a new terminal; do not drop the current session)
ssh -p 2222 -i ~/.ssh/id_ed25519 admin@server_ip
# 2. confirm password login is rejected
ssh -p 2222 admin@server_ip -o PreferredAuthentications=password
# should show Permission denied (publickey)
# 3. check auth logs for anomalies
sudo journalctl -u ssh -f | grep "Failed password"
All three checks pass means hardening is live. Then test root via sudo -i. Only then close the old port.
FAQ
Q1: How to deploy keys on a new server after disabling password login?
Two steps: keep password auth during setup, enable keys, run ssh-copy-id. After key login works, set PasswordAuthentication no and reload. Never disable passwords on a host with no key deployed — you will lock yourself out.
Q2: Connection timeout after changing the port?
Check cloud security group/firewall allows the new port first. Then sshd -t and systemctl status sshd. If the host is unreachable, use vendor VNC and revert Port 22. Add the new listen port, verify, then drop the old one.
Summary
Principles: key auth first, least privilege, whitelist, fail2ban/firewall as backstop. After this, SSH is a key-only door, not a blast target. Pair with fail2ban and firewall rules; brute-force and unauthorized login risk drops to near zero. Bake these steps into the init script. Re-check sshd_config after every OS upgrade — packages can reset it.