SSH hardening and fail2ban on a VPS
A short security pass that stops 99% of the brute-force noise on a public-IP VPS. SSH keys only, fail2ban, port choice, and what not to bother with.

Any VPS with a public IP gets thousands of SSH login attempts a day from compromised hosts running hydra. Most of them fail because you used a key, not a password. The remaining noise is annoying enough to address. Here's a no-nonsense hardening pass.
What this protects against
- Automated SSH brute-force from botnets
- Dictionary attacks on services you forgot were exposed
- Log spam that hides real signals
What it doesn't protect against
- A determined targeted attacker
- A zero-day in OpenSSH (use unattended-upgrades for that)
- An attacker who already has your SSH private key
If you assume you face the second category, the rest of the security work is bigger than fail2ban and out of scope here.
SSH config first, fail2ban second
The config below turns off password logins, blocks root, and restricts SSH to a single non-root user. Apply it before that user exists and works with a key, and you have locked everyone out, including yourself. So the hard prerequisite is a non-root sudo user with a tested SSH key. If you already have one, skip ahead to the config. If you're still working as root on a fresh box, create one now:
sudo adduser deploy
sudo usermod -aG sudo deploy
Then install your public key for that user, from your workstation. If you don't have a keypair yet, generate one first:
ssh-keygen -t ed25519
Then copy it over:
ssh-copy-id deploy@your.vps.ip
ssh-copy-id logs in with the deploy account's password, so it relies on password authentication still being enabled on the server, which it is at this point: we haven't turned it off yet. If your image was provisioned key-only and passwords are already refused, install the key from your existing root session instead, pasting in the contents of your workstation's ~/.ssh/id_ed25519.pub:
sudo install -d -m 700 -o deploy -g deploy /home/deploy/.ssh
echo 'ssh-ed25519 AAAA...your-key... you@workstation' | sudo tee /home/deploy/.ssh/authorized_keys
sudo chown deploy:deploy /home/deploy/.ssh/authorized_keys
sudo chmod 600 /home/deploy/.ssh/authorized_keys
Now open a new terminal and verify that both the key login and sudo actually work:
ssh deploy@your.vps.ip
sudo -v
The login should drop you into a shell without asking for a password, and sudo -v should accept the account's password. Do not go any further until both succeed.
Now the config. One detail matters more than it looks: sshd uses the first value it finds for each keyword, and the drop-ins in /etc/ssh/sshd_config.d/ are read in lexical order before the main file. Cloud images typically ship a 50-cloud-init.conf in there with PasswordAuthentication yes, and a file named 99-anything sorts after it and loses silently. So name yours to sort first. Create /etc/ssh/sshd_config.d/00-hardening.conf:
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
ChallengeResponseAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
MaxAuthTries 3
LoginGraceTime 20
ClientAliveInterval 300
ClientAliveCountMax 2
AllowUsers deploy
Replace deploy with your actual sudo user. Before you reload, keep your current session open: if anything in this file is wrong, that open session is your only way back in. Locking yourself out is a real risk; the GetVPS console is the back door, but it's awkward. Reload:
sudo systemctl reload ssh
Then check what sshd actually resolved, not just what you wrote:
sudo sshd -T | grep -Ei 'permitrootlogin|passwordauthentication|authenticationmethods'
You want permitrootlogin no, passwordauthentication no and authenticationmethods publickey. If a yes shows up, another drop-in is overriding you; find it with grep -ri passwordauthentication /etc/ssh/sshd_config.d/ and remove the conflicting line, then reload and check again.
Now test from a second terminal, before logging out of your current session: confirm ssh deploy@your.vps.ip still gets you in, then carry on.
Check UFW is actually on
Two things later in this guide assume UFW is enforcing rules: the optional port move, and fail2ban's ban action. On a stock Ubuntu install UFW is present but inactive, and an inactive UFW accepts rules without enforcing any of them, so everything would look configured while doing nothing. Check where you stand:
sudo ufw status
If it says Status: inactive, enable it. Enabling a firewall that has no SSH allow rule cuts off your own session, so add the allow rule first and only then turn the firewall on:
sudo ufw allow OpenSSH
sudo ufw enable
Run sudo ufw status again to confirm it's active and OpenSSH is allowed, then confirm from a second terminal that ssh deploy@your.vps.ip still gets you in.
Should you change the port?
Honest answer: it doesn't matter much. Moving SSH from 22 to 2222 cuts the noise in your logs by ~95% because most botnets only scan well-known ports. It does not protect you against a targeted scanner; nmap -p- finds the new port in minutes. So:
- Change it if log-noise hygiene matters to you.
- Don't change it if you'd rather have firewall rules that work without remembering a non-standard port.
If you change it, do it in an order that keeps port 22 working until port 2222 is proven. First, open the new port in UFW:
sudo ufw allow 2222/tcp
Then edit /etc/ssh/sshd_config.d/00-hardening.conf to listen on both ports for now:
Port 22
Port 2222
Apply the change. On Ubuntu 24.04 SSH is socket-activated, so a plain systemctl reload ssh does not rebind ports; you need to regenerate the socket config and restart the socket (existing sessions survive this):
sudo systemctl daemon-reload
sudo systemctl restart ssh.socket
From a second terminal, confirm the new port actually answers:
ssh -p 2222 deploy@your.vps.ip
Do not run the next block until that login succeeds. It stops sshd listening on 22 and deletes the firewall rule that allows it, so if port 2222 turns out to be unreachable, from a typo in the config or a filter somewhere on the path, you are locked out and headed for the GetVPS console. Once the port 2222 login works, remove the Port 22 line from 00-hardening.conf, then:
sudo systemctl daemon-reload
sudo systemctl restart ssh.socket
sudo ufw delete allow OpenSSH
If UFW was already active before this guide and sudo ufw status lists the SSH rule as 22/tcp rather than OpenSSH, delete that rule name instead: sudo ufw delete allow 22/tcp.
Install fail2ban
sudo apt update
sudo apt install -y fail2ban
The package on Ubuntu 24.04 ships sensible defaults but doesn't enable any jails. Create /etc/fail2ban/jail.local:
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
banaction = ufw
ignoreip = 127.0.0.1/8 ::1 203.0.113.42
[sshd]
enabled = true
port = ssh
mode = aggressive
logpath = %(sshd_log)s
backend = systemd
Replace 203.0.113.42 in ignoreip with your own home or office IP. That's the always-allowed list, and it's the line that stops fail2ban from ever banning you. On a dynamic-IP home network, use a VPN with a stable address and ignore that instead.
If you moved SSH to 2222 in the previous section, change port = ssh to port = 2222. The ssh service name resolves to port 22, so leaving it as-is would have fail2ban banning attempts on a port you no longer use. If you stayed on 22, port = ssh is correct as written.
mode = aggressive catches more pre-auth failures (probes that don't even try a password). backend = systemd reads the journal directly, which is the right choice on modern Ubuntu.
One check before you start it: once fail2ban is running it will ban any IP that fails repeatedly, including yours, and a self-ban locks you out for the full bantime. Make sure your real IP is in that ignoreip line before going further. Then start it:
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd
The status command should show the sshd jail running. Run it again after a few minutes; on any public IP you'll watch banned addresses accumulate.
Other jails worth enabling
For a public-facing Nginx, drop these in jail.local:
[nginx-http-auth]
enabled = true
[nginx-botsearch]
enabled = true
[nginx-bad-request]
enabled = true
For Postfix:
[postfix]
enabled = true
mode = aggressive
Reload after each change:
sudo systemctl reload fail2ban
What not to bother with
- DenyHosts is unmaintained. Use fail2ban.
- Knock-knock / port knocking is security theatre against any modern attacker.
- Banning whole countries breaks more than it fixes. If you must, do it at the firewall level via maxmind GeoIP, not in fail2ban.
- Weird SSH ciphers and MACs are mostly cargo-culted from 2014 guides. The OpenSSH defaults on Ubuntu 24.04 are fine.
Verify
From an external host, run (add -p 2222 if you moved the port):
ssh root@your.vps.ip
You should see Permission denied (publickey) immediately.
Next, prove that bans actually happen. You can't "try a few wrong passwords" here: you disabled password authentication earlier, so the server never offers a prompt. Instead, generate the failures fail2ban counts by asking for an auth method the server won't grant. Before you do, know that the IP you test from will be banned for the full hour of bantime, so use a host that is not behind the same public IP as your workstation and not listed in ignoreip; a phone hotspot or another VPS works, and a test from an ignoreip address will silently never ban. From that host, run this six times (add -p 2222 if you moved the port):
ssh -o PubkeyAuthentication=no -o PreferredAuthentications=password baduser@your.vps.ip
Each attempt fails pre-auth, and mode = aggressive counts exactly this kind of failure. Back on the server, check the jail:
sudo fail2ban-client status sshd
The test host's IP should show up in the banned list. Unban it when you're done, swapping in the IP you tested from:
sudo fail2ban-client set sshd unbanip 198.51.100.7
That's it. Five minutes of config that stops the noise and keeps the signal.
Continue reading