Securing an Ubuntu VPS: SSH, firewall, and fail2ban

A VPS exposed to the internet gets scanned within minutes of creation. Here is the hardening checklist to run before you put anything on it.

Shield protecting a server, with connection attempts blocked

A freshly assigned public IPv4 address receives its first SSH authentication attempts in under ten minutes. These are not targeted attacks: they are automated sweeps trying root/123456 across the whole address space. The good news is that an hour of configuration is enough to make them harmless.

This guide targets a freshly provisioned Ubuntu 24.04 LTS VM, but the principles apply to Debian 12 and 13 with a few package names changed.

1. Update the system

A base image is frozen at publication time. The very first command you run should apply the security fixes that have accumulated since.

bash
sudo apt update && sudo apt full-upgrade -y
sudo reboot

The reboot is only needed if the kernel was updated. ls /var/run/reboot-required tells you instead of leaving you to guess.

2. Create an unprivileged user

Working as root day to day removes the last safety net: one mis-pasted command becomes irreversible. Create a named admin account, grant it sudo, then close the door on root.

bash
sudo adduser florian
sudo usermod -aG sudo florian

3. Move to SSH keys

A password can be guessed; an Ed25519 key cannot. Generate the pair on your workstation, never on the server, the private key must not leave your machine.

bash
# on your workstation
ssh-keygen -t ed25519 -C "florian@laptop"
ssh-copy-id florian@2602:xxxx:xxxx::10

Confirm that key-based login works in a second terminal before disabling password authentication. Keeping the current session open is what saves you from locking yourself out.

4. Harden the SSH server

On Ubuntu 24.04, do not edit /etc/ssh/sshd_config directly: a package update can overwrite your changes. Drop a dedicated file into the include directory instead.

bash
sudo tee /etc/ssh/sshd_config.d/99-hardening.conf <<'EOF'
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
LoginGraceTime 20
AllowUsers florian
EOF

sudo sshd -t && sudo systemctl restart ssh

Worth knowing: Ubuntu 24.04 starts SSH through socket activation (ssh.socket). Changing Port in the config is no longer enough; you have to run sudo systemctl edit ssh.socket and set ListenStream= there. That said, moving the port reduces log noise, not actual risk, the protection comes from the keys.

sshd -t validates the syntax before the restart. It is the guard rail that separates a restarted server from an unreachable one.

5. Close the firewall (IPv4 and IPv6)

UFW applies its rules to both address families as long as IPV6=yes is set in /etc/default/ufw, which it is on Ubuntu. Check it anyway: a server filtered on IPv4 but open on IPv6 is an open server.

bash
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw limit OpenSSH
sudo ufw enable
sudo ufw status verbose

Use limit rather than allow: UFW then rejects any address attempting more than six connections in thirty seconds. Only open ports 80 and 443 (sudo ufw allow 'Nginx Full') on the day a service actually listens on them.

6. Install fail2ban

fail2ban reads logs and bans addresses that fail too often. On Ubuntu 24.04 the logs come from systemd, say so explicitly, otherwise the jail stays idle.

bash
sudo apt install -y fail2ban

sudo tee /etc/fail2ban/jail.local <<'EOF'
[DEFAULT]
backend  = systemd
bantime  = 1h
findtime = 10m
maxretry = 4

[sshd]
enabled = true
EOF

sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd

7. Automate security patches

A server forgotten for six months is a vulnerable server. Unattended security updates beat supervised updates that never happen.

bash
sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
sudo unattended-upgrade --dry-run --debug

For kernels, enable Unattended-Upgrade::Automatic-Reboot "true"; with a time window (Automatic-Reboot-Time "04:00";) in /etc/apt/apt.conf.d/50unattended-upgrades if the service tolerates a nightly restart.

8. Verify the result

Three commands confirm that your attack surface is the one you think it is.

bash
# what is actually listening, IPv4 and IPv6
sudo ss -tulpen

# recent authentication attempts
sudo journalctl -u ssh --since "1 hour ago" | tail -30

# firewall state, both families
sudo ufw status numbered

If ss shows a service listening on 0.0.0.0 or [::] that you did not deploy, a database, a cache, bind it to 127.0.0.1 and ::1 rather than relying on the firewall alone.

Checklist

  • System updated, rebooted if the kernel changed.
  • Non-root account with sudo, key login verified.
  • PermitRootLogin no and PasswordAuthentication no.
  • UFW defaulting to deny, active on IPv4 and IPv6.
  • fail2ban running with the systemd backend.
  • Automatic security updates enabled.
  • A backup taken once the configuration is done.

That last point matters as much as the rest: our VPS plans include 7 manual backups, you trigger them whenever you want, and the best moment is right after a successful hardening pass. It is the restore point you will come back to the day an experiment goes sideways.