Why Bother on a Home Network?
SSH is the management plane for your entire lab — Proxmox, NAS, every VM and container. Even if nothing is exposed to the internet, hardening it matters:
- One compromised device pivots to everything. A phished laptop or a sketchy IoT gadget on the same LAN can reach port 22 on all of it.
- Exposure changes over time. The tunnel or port forward you add next year inherits whatever SSH posture you set up today.
- It's free practice. The exact same steps apply to any VPS you ever rent, where bots start hammering port 22 within minutes of boot.
Step 1: Switch to Key-Based Auth
Generate a modern ed25519 key on your workstation and push it to the server:
# On your workstation
ssh-keygen -t ed25519 -a 100 -C "michael@workstation"
# Copy it to each server
ssh-copy-id [email protected]
# Verify key login works BEFORE disabling passwords
ssh [email protected]
Use a passphrase on the key and let ssh-agent (or your OS keychain) cache it. One key per device you SSH from — not one key copied everywhere — so you can revoke a lost laptop without rotating everything.
Step 2: Harden sshd_config
Drop overrides into /etc/ssh/sshd_config.d/99-hardening.conf instead of editing the main file — package upgrades won't clobber them:
# /etc/ssh/sshd_config.d/99-hardening.conf
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
PubkeyAuthentication yes
# Only accounts that should ever SSH in
AllowUsers michael ansible
MaxAuthTries 3
LoginGraceTime 20
ClientAliveInterval 300
ClientAliveCountMax 2
# Disable what you don't use
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding yes # keep if you use SSH tunnels; else "no"
Validate and reload — and keep your current session open until a fresh login works:
sudo sshd -t # syntax check — do not skip
sudo systemctl reload ssh
# From a SECOND terminal, confirm you can still get in
ssh [email protected]
# Confirm passwords are actually rejected
ssh -o PubkeyAuthentication=no [email protected]
# Expect: Permission denied (publickey).
Should you change the port? Moving SSH to 2222 cuts log noise from drive-by scanners but adds zero real security. Do it for tidiness if you like — just document it, and remember Fail2ban and your firewall rules need the same port.
Step 3: Fail2ban
Fail2ban tails the auth log and firewalls IPs that keep failing. With password auth off it's belt-and-suspenders, but it also throttles bots probing for weak keys and covers other services later (NPM, Authelia, Vaultwarden all have community jails).
sudo apt install fail2ban
Never edit jail.conf — create /etc/fail2ban/jail.local:
[DEFAULT]
# Never ban your own management network
ignoreip = 127.0.0.1/8 192.168.1.0/24
bantime = 1h
findtime = 10m
maxretry = 5
# Repeat offenders get escalating bans
bantime.increment = true
bantime.maxtime = 1w
[sshd]
enabled = true
port = ssh # change if you moved the port
backend = systemd # Debian 12+/Ubuntu 22.04+ log to journald
sudo systemctl enable --now fail2ban
# Check the jail is live
sudo fail2ban-client status sshd
The ignoreip line is the one that saves you at 2am. Locking yourself out of a remote VPS is annoying; locking yourself out of the Proxmox host that runs your DNS is a bad evening.
Managing Bans
# Who's banned right now?
sudo fail2ban-client status sshd
# Unban a specific IP
sudo fail2ban-client set sshd unbanip 203.0.113.50
# Watch it work
sudo journalctl -u fail2ban -f
Rolling It Out Lab-Wide
- Start with one non-critical VM, confirm the workflow, then repeat. The Proxmox host itself goes last, after everything else has been boring for a week.
- Keep one break-glass path: the Proxmox console (or IPMI) gets you in even if SSH config is broken — that's your safety net, not a reason to skip testing.
- Same config everywhere: the
sshd_config.ddrop-in andjail.localare two small files — perfect first candidates for Ansible or even a simple scp loop.
Common Pitfalls
- Disabling passwords before testing keys. Always verify a key login from a second terminal before you reload sshd with
PasswordAuthentication no. AllowUserstypos: it silently locks out everyone not listed — including you. Check spelling againstwhoami.- Wrong Fail2ban backend: on modern Debian/Ubuntu there is no
/var/log/auth.logby default; withoutbackend = systemdthe sshd jail sees nothing and bans nothing. - Banning yourself through a proxy: if you SSH via a jump host or VPN gateway, failed attempts all appear to come from that one IP — add it to
ignoreip.
Validation Checklist
- Key-based login works from every device you administer from
ssh -o PubkeyAuthentication=no user@hostis refusedssh root@hostis refusedsudo sshd -tpasses on every hostfail2ban-client status sshdshows the jail active with your log backend- A deliberate string of bad logins from a test IP gets banned — and unbanned with
unbanip - Your management subnet is in
ignoreipand the console break-glass path works
Next step up the stack: put Authelia SSO in front of your web apps so HTTP gets the same single-front-door treatment SSH just got.