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.
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.
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.
# 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.
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.
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.
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.
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.
# 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 noandPasswordAuthentication 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.