
Every self-hosting guide ends with a paragraph called "lock the box down" and then stops. This is that paragraph, written out properly.
None of it is exotic. You are not defending against a targeted attacker - you are defending against the automated scanners that find a new IP within minutes of it existing, and against yourself in six months when the box has not been patched. Thirty minutes now covers both.
It applies to short-lived boxes too, and those are the ones people skip. A machine rented for an afternoon - a throwaway test server, or a GPU instance for a batch job - is exposed the moment it has a public IP, and gets a fraction of the attention a server you keep for a year does.
First, look at what is already happening
Before changing anything, see the traffic your box is already receiving. On a server that has been up for a day:
sudo journalctl -u ssh --since "24 hours ago" | grep -c "Invalid user"
sudo lastb | head -20
On a fresh VPS with password authentication enabled, those numbers are usually in the thousands. That is the baseline. Everything below is about making that traffic irrelevant rather than merely quieter.
1. SSH keys - and the two traps
Key-only authentication is the single change that matters most. Everything else on this list is defence in depth; this one closes the front door.
From your own machine:
ssh-copy-id user@YOUR_SERVER_IP
ssh user@YOUR_SERVER_IP # confirm it works BEFORE disabling passwords
Then, on the server, the usual advice is to edit /etc/ssh/sshd_config:
PasswordAuthentication no
PermitRootLogin prohibit-password
KbdInteractiveAuthentication no
On a modern Ubuntu cloud image, that edit does nothing. Two reasons, and both catch experienced people.
Trap 1: OpenSSH is first-match-wins
Most config formats let a later line override an earlier one. OpenSSH does the
opposite - the first occurrence of a keyword wins. And sshd_config places
its Include /etc/ssh/sshd_config.d/*.conf line at the top of the file, before
anything else is set.
So every drop-in file in sshd_config.d/ takes precedence over the main config,
not the other way round. Ubuntu cloud images ship
/etc/ssh/sshd_config.d/50-cloud-init.conf, and it commonly contains:
PasswordAuthentication yes
Which means your careful PasswordAuthentication no further down the main file
is read second and ignored. Check before you trust it:
ls /etc/ssh/sshd_config.d/
sudo sshd -T | grep -iE "passwordauthentication|permitrootlogin|pubkeyauth"
sshd -T prints the effective configuration after all includes are resolved.
It is the only output worth believing. Put your settings in a drop-in that sorts
early, or fix the cloud-init file directly:
sudo tee /etc/ssh/sshd_config.d/00-hardening.conf <<'EOF'
PasswordAuthentication no
PermitRootLogin prohibit-password
KbdInteractiveAuthentication no
EOF
sudo sshd -t && sudo systemctl reload ssh
sudo sshd -T | grep -i passwordauthentication # verify again
Note sshd -t before the reload. It validates the config; skipping it is how
people lock themselves out of a machine with no console.
Trap 2: socket activation ignores your Port
Since Ubuntu 22.10, sshd is socket-activated. ssh.socket owns the listening
port and starts sshd per connection. The consequence:
PortandListenAddressinsshd_configare not used when sshd is socket-activated.
So changing Port 2222 and restarting ssh.service leaves the server listening
on 22, and you conclude the config is broken. To actually change the port you
edit the socket unit:
sudo systemctl edit ssh.socket
[Socket]
ListenStream=
ListenStream=2222
sudo systemctl daemon-reload
sudo systemctl restart ssh.socket
The empty ListenStream= is required - it clears the inherited value rather than
adding a second port.
Whether you should change the port at all is covered further down.
2. Stop using root
If you are still logging in as root, stop:
adduser deploy
usermod -aG sudo deploy
rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy
Log in as deploy, confirm sudo works, then set
PermitRootLogin prohibit-password (or no) and reload.
The benefit is not that root is magically dangerous. It is that sudo leaves an
audit trail and forces a deliberate pause before destructive commands.
3. A firewall - and knowing which one
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH # or 'sudo ufw allow 2222/tcp' if you moved it
sudo ufw allow 51820/udp # WireGuard, if this is a VPN box
sudo ufw enable
sudo ufw status verbose
Allow SSH before enabling. ufw enable takes effect immediately and does not
care that you are connected over the port it is about to block.
If your VPS is on a provider with its own network firewall -
Lightsail's instance firewall, DigitalOcean Cloud
Firewalls, EC2 security groups - that is a separate layer that sits in front
of ufw. A port has to be open in both. This is the
most common reason a correctly configured service is unreachable, and the most
common reason a service you thought was firewalled is not.
4. Automatic security updates
The box will outlive your interest in it. Make that safe:
sudo apt update && sudo apt upgrade -y
sudo apt install unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgrades
By default this applies security updates only, which is the right scope - you want CVEs closed without a feature upgrade rebooting your VPN at 3am. If you do want automatic reboots for kernel updates, set them explicitly and pick the hour:
// /etc/apt/apt.conf.d/50unattended-upgrades
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";
Check it is actually running a week later:
sudo unattended-upgrade --dry-run --debug
cat /var/log/unattended-upgrades/unattended-upgrades.log
An unattended-upgrades install that silently stopped working is worse than not having it, because you stopped checking.
5. fail2ban or CrowdSec - with realistic expectations
sudo apt install fail2ban
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd
Here is the honest framing. Once password authentication is off, brute-force attempts against SSH cannot succeed. fail2ban is then not protecting your authentication - it is reducing log volume and CPU spent on doomed handshakes. That is worth having, but it is housekeeping, not security.
Where it earns its place is any service that still has a password: a web admin panel, a mail server, a database with an exposed port. If SSH is genuinely your only listener and it is key-only, you can skip this and lose very little.
CrowdSec is the more interesting option - it shares attacker IPs across a community feed, so you benefit from blocks other people's servers triggered. It costs more setup and more memory. On a 512 MB VPN box, that trade is not obviously worth it.
6. Should you move the SSH port?
Moving SSH to a non-standard port does not make it more secure. Anyone scanning seriously will find it, and the service on the other end is exactly as strong or weak as it was on 22.
What it does is remove nearly all of the automated noise, because commodity scanners hit 22 and move on. If you find value in logs you can actually read, that is a real benefit. Call it what it is - noise reduction, not hardening - and do not let it substitute for key-only auth.
Verify, do not assume
Run these before you close the terminal. Each one checks reality rather than intent:
sudo sshd -T | grep -iE "passwordauth|permitrootlogin|port "
sudo ufw status verbose
sudo systemctl is-enabled unattended-upgrades
ss -tulpn | grep LISTEN # what is ACTUALLY listening
That last one deserves a moment. ss -tulpn shows every open socket and the
process behind it. If something is bound to 0.0.0.0 that you did not intend to
expose - a database, a metrics endpoint, a container's admin port - this is where
you find out, and it is the check most people never run.
What this does not cover
Be clear about the boundary. Thirty minutes buys you: no password brute-force, no root login, nothing exposed that you did not choose, and patches applied without you.
It does not cover application vulnerabilities in whatever you are actually running, supply-chain risk in the packages you install, or anyone with access to your private key. If the box holds something that matters, the next steps are backups you have tested restoring and a second factor on SSH - not more firewall rules.
But almost nobody gets compromised by an exotic attack. They get compromised by a
password-authenticating root account on a machine that has not seen apt upgrade
since it was created. That is what this list is for.
Cover photo by Cong Long Vu on Unsplash.



