SSH Hardening & Fail2ban

Lock down the front door of every box in your lab: key-only auth, a tightened sshd_config, and automatic banning of brute-force attempts.

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.d drop-in and jail.local are 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.
  • AllowUsers typos: it silently locks out everyone not listed — including you. Check spelling against whoami.
  • Wrong Fail2ban backend: on modern Debian/Ubuntu there is no /var/log/auth.log by default; without backend = systemd the 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@host is refused
  • ssh root@host is refused
  • sudo sshd -t passes on every host
  • fail2ban-client status sshd shows 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 ignoreip and 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.

- Crafted by Axiom|Spectre