Every time I spin up a new VPS I run through the same set of steps before I put anything on it. It takes about 20 minutes and covers the most common attack vectors for an internet-facing Linux server. None of this is exotic – it’s mostly well-established practice – but having a checklist means I don’t skip a step because I’m in a hurry.
This is for Ubuntu 22.04 LTS or 24.04 LTS. Most steps work on Debian with minor adjustments.
1. Create a non-root user and lock down SSH
Log in as root one last time, create a regular user, and add it to the sudo group:
adduser deploy
usermod -aG sudo deploy
Copy your SSH public key to the new user (from your local machine):
ssh-copy-id deploy@your-server-ip
Or paste it manually into /home/deploy/.ssh/authorized_keys with permissions 700 on the directory and 600 on the file.
Now edit /etc/ssh/sshd_config and set these values:
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
X11Forwarding no
Reload SSH – but don’t close your current session yet:
systemctl reload sshd
Open a new terminal and verify you can log in as your new user before closing the root session. Getting locked out here is embarrassing and fixable only through the provider’s console.
2. Configure ufw
Ubuntu ships with ufw (Uncomplicated Firewall) which wraps iptables into something manageable. Default policy is deny incoming, allow outgoing:
ufw default deny incoming
ufw default allow outgoing
ufw allow ssh
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
If you’re running something else – a game server, a custom port, a mail relay – add those rules before enabling. Check status with ufw status verbose.
If SSH is on a non-standard port, use ufw allow PORT/tcp instead of ufw allow ssh. The named rule just resolves to port 22.
3. Install and configure fail2ban
fail2ban watches log files for repeated failed authentication attempts and temporarily bans the source IP via iptables. It won’t stop a determined attacker but it kills the noise from automated scanners almost completely.
apt install fail2ban -y
Create a local override file rather than editing the defaults directly:
cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
Edit /etc/fail2ban/jail.local and adjust the [sshd] section:
[sshd]
enabled = true
port = ssh
maxretry = 5
bantime = 1h
findtime = 10m
Start and enable it:
systemctl enable --now fail2ban
Check ban status with fail2ban-client status sshd. See the Wikipedia article on Fail2ban for a quick overview of how the detection mechanism works.
4. Enable unattended security upgrades
apt install unattended-upgrades -y
dpkg-reconfigure --priority=low unattended-upgrades
Answer yes when prompted. This configures the system to automatically apply security updates only – not dist-upgrades or optional package updates, which is the conservative setting you want on a server.
Check the config in /etc/apt/apt.conf.d/50unattended-upgrades if you want to tweak what gets installed. The Ubuntu documentation on automatic security updates has good coverage of the available options.
5. A few sysctl tweaks
Edit /etc/sysctl.conf (or drop a file in /etc/sysctl.d/) and add:
# Disable IP source routing
net.ipv4.conf.all.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0
# Ignore ICMP redirects
net.ipv4.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
# Enable SYN flood protection
net.ipv4.tcp_syncookies = 1
# Disable IPv6 if you're not using it
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
Apply without rebooting:
sysctl -p
The IPv6 disable lines are optional – skip them if you’re using IPv6 or might need it. The rest are low-risk defaults that reduce the attack surface without affecting normal traffic.
Bonus: change the default SSH port
Controversial suggestion: moving SSH off port 22 doesn’t add real security, but it does dramatically reduce the noise in your auth logs, which makes it easier to spot anything genuinely suspicious. If you do this, update your ufw rule before restarting sshd:
ufw allow 2222/tcp
ufw delete allow ssh
Then edit /etc/ssh/sshd_config and change Port 22 to Port 2222 (or whatever you pick). Remember to update your SSH client config (~/.ssh/config on your local machine) with Port 2222 for the host, otherwise every login requires ssh -p 2222 deploy@host.
If you’re on a cloud provider that uses a security group layer (AWS, GCP, DigitalOcean), update that rule too – the provider’s firewall is separate from ufw and blocking port 22 at the OS level while the cloud firewall still allows it doesn’t actually close anything.
Done – for now
Twenty minutes and you’ve covered the basics: no root SSH login, key-only authentication, a firewall with a minimal open port list, automatic brute-force banning, automatic security patches, and a handful of kernel network settings. This won’t protect against every possible attack, but it handles the vast majority of automated scanning and credential stuffing attempts you’ll see against any internet-facing server. From here, hardening gets more specific to whatever you’re actually running – web server config, database access controls, AppArmor profiles, and so on.