VPS Hosting for Developers - Part 2: Hardening a Fresh Server
A practical hardening checklist for a new Linux VPS: users, SSH keys, firewall rules, automatic updates, and the commands to verify each step.
VPS Hosting for Developers
Part 2 of 4
A fresh VPS starts scanning itself within minutes of coming online. The goal of this part is not paranoia; it is to remove the handful of defaults that make a server easy to compromise, then verify that each change actually took effect.
Work in order, and keep a second terminal open so a mistake in SSH configuration does not lock you out.
Before You Connect
Take a provider snapshot before making changes. It is the fastest undo button you will ever have. Then connect as root and create a working user:
adduser deploy
usermod -aG sudo deploy
From this point on, use the deploy user for everything. Root exists for emergencies.
SSH Keys Instead of Passwords
On your local machine, generate a key if you do not have one:
ssh-keygen -t ed25519 -C "deploy@my-vps"
ssh-copy-id deploy@203.0.113.10
Then confirm key login works in a new terminal before touching the server configuration. Editing sshd_config first and testing second is how people lock themselves out.
Harden the SSH Daemon
Edit /etc/ssh/sshd_config and set:
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
KbdInteractiveAuthentication no
Validate the file before restarting the service:
sudo sshd -t
sudo systemctl restart ssh
Leave the original session open until the new one succeeds. If something is wrong, you still have access to fix it.
Turn On the Firewall
ufw is a friendly front end for nftables and is enough for most servers:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose
Only 22, 80, and 443 are open. Databases and internal services stay on localhost and are reached through SSH tunnels or a private network — never exposed directly.
Automatic Security Updates
Manual patching does not survive contact with a busy week. On Debian and Ubuntu:
sudo apt update
sudo apt install unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgrades
Check the configuration in /etc/apt/apt.conf.d/20auto-upgrades to confirm daily update checks and automatic security installs are on. Reboots still need a window you control; schedule them or monitor for the “reboot required” flag.
A Few Extra Layers
- fail2ban adds temporary bans after repeated failed logins:
sudo apt install fail2ban sudo systemctl enable --now fail2ban - Swap keeps small instances alive during memory spikes:
sudo fallocate -l 2G /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfile && sudo swapon /swapfile echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab - Time zone and time sync make logs readable and TLS happier:
sudo timedatectl set-timezone UTC sudo timedatectl set-ntp true
Verify, Don’t Assume
A hardening pass is only complete when you check it from the outside:
# Who is listening, and on which address?
sudo ss -tulpn
# Is the firewall really filtering?
sudo ufw status numbered
# Any failed login attempts already?
sudo journalctl -u ssh --since "1 hour ago" | grep -i failed
An external port scanner against your host should show exactly the ports you intended, and nothing else.
Conclusion
Hardening is a short list of defaults done properly: a non-root user, keys only, a deny-by-default firewall, and updates on autopilot. In Part 3 we will put this server to work with Docker and Caddy, and get automatic HTTPS without touching a certificate again.