Securing a Fresh Ubuntu VPS: SSH Keys, UFW and Fail2ban
Lock down a new Ubuntu VPS step by step — SSH keys, disabling passwords and root login, UFW firewall rules, Fail2ban, automatic updates, and the Docker firewall gotcha.
On this page
Within minutes of a server getting a public IP address, automated bots start trying to log in. Most of these attacks are crude — guessing common passwords for common usernames — and a few simple measures make them hopeless. This guide covers the essentials for an Ubuntu VPS: SSH keys, a firewall, Fail2ban and automatic updates.
Step 1: Use SSH keys, not passwords
An SSH key pair consists of a private key (kept on your computer) and a public key (placed on the server). Keys are far too long to guess, which makes brute-force attacks pointless.
On your computer:
ssh-keygen -t ed25519 -C "laptop"
Protect the key with a passphrase when prompted. Then copy the public key to your server’s user:
ssh-copy-id deploy@YOUR_SERVER_IP
If ssh-copy-id isn’t available (for example on Windows without WSL), append the contents of your .pub file to ~/.ssh/authorized_keys on the server and make sure permissions are strict: chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys.
Test the key login in a new terminal before changing anything else.
Step 2: Disable password and root login
Edit /etc/ssh/sshd_config:
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
PubkeyAuthentication yes
The override trap
Ubuntu reads extra configuration from /etc/ssh/sshd_config.d/*.conf, and settings there are read before the main file — the first value OpenSSH finds for most options wins. Some cloud images ship a file such as 50-cloud-init.conf containing PasswordAuthentication yes, which silently keeps passwords enabled no matter what you put in sshd_config.
Check:
sudo grep -ri passwordauthentication /etc/ssh/sshd_config.d/
Edit or remove any line that enables passwords. Then validate the configuration and restart:
sudo sshd -t && sudo systemctl restart ssh
sshd -t checks for syntax errors, so a typo can’t lock you out. Confirm the effective settings:
sudo sshd -T | grep -E 'passwordauthentication|permitrootlogin'
Keep an existing session open while you test a fresh login. If something goes wrong, your provider’s web VNC console (included with Kerit Cloud VPS plans) lets you fix it without SSH.
Step 3: Enable the UFW firewall
UFW (Uncomplicated Firewall) ships with Ubuntu. The safe order is: set defaults, allow SSH, then enable.
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw enable
Open only what you run:
sudo ufw allow 80,443/tcp # web server
sudo ufw allow from 203.0.113.10 to any port 3306 proto tcp # database, one trusted IP only
sudo ufw status numbered
sudo ufw delete 3 # remove a rule by number
Restricting sensitive ports to specific IPs — databases, admin panels, monitoring — is one of the most effective protections available.
The Docker gotcha
Docker manipulates iptables directly when you publish container ports (-p 8080:80), and those rules take effect before UFW’s. A container port you think is blocked by UFW may be wide open to the internet.
The simplest fixes:
- Publish ports on localhost only when a reverse proxy sits in front:
-p 127.0.0.1:8080:80. - Don’t publish ports that only other containers need; use a Docker network instead.
- Use your provider’s edge firewall as an outer layer.
See installing Docker and Docker Compose.
Step 4: Install Fail2ban
Even with passwords disabled, bots keep knocking and fill your logs. Fail2ban watches logs and temporarily bans IPs that fail repeatedly.
sudo apt install -y fail2ban
Create /etc/fail2ban/jail.local (never edit jail.conf, which updates overwrite):
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
backend = systemd
[sshd]
enabled = true
Start it and check:
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd
Fail2ban can also protect web servers and other services with additional jails.
Step 5: Keep the system patched
Most compromised servers weren’t hacked with clever exploits — they were running software with known, fixed vulnerabilities. Enable automatic security updates:
sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgrades
Reboot periodically for kernel updates. cat /var/run/reboot-required tells you when one is pending.
Step 6: Reduce what’s exposed
- List listening services:
sudo ss -tulpn. Anything listening on0.0.0.0or[::]is reachable from the internet unless the firewall blocks it. - Bind internal services to localhost. Databases, Redis and admin tools usually only need
127.0.0.1. - Remove what you don’t use. Every installed service is something that needs patching.
Step 7: Don’t forget IPv6
If your VPS has IPv6 — Kerit Cloud plans include a /64 subnet — make sure the firewall covers it. UFW handles IPv6 when IPV6=yes is set in /etc/default/ufw (the default on Ubuntu). A service blocked on IPv4 but open on IPv6 is still open.
Beyond the basics
Once these are in place, consider:
- Changing the SSH port — reduces log noise, but isn’t real security on its own.
- Restricting SSH to your IPs via UFW or your provider’s edge firewall.
- Two-factor authentication for SSH, and short-lived keys for teams.
These are covered in hardening SSH beyond the basics. For network-level attacks, Kerit Cloud VPS plans include Cloudflare Magic Transit and OVH TCP Shield DDoS protection, plus an edge firewall — see using an edge firewall and port policies.
A quick audit
sudo sshd -T | grep -E 'passwordauthentication|permitrootlogin' # both "no"
sudo ufw status verbose # active, minimal rules
sudo fail2ban-client status sshd # running
sudo ss -tulpn # only expected listeners
systemctl status unattended-upgrades --no-pager # active
Summary
Secure a new Ubuntu VPS by switching to SSH keys and disabling password and root login — checking sshd_config.d for overrides and validating with sshd -t. Enable UFW with default-deny and only the ports you need, restricting sensitive ones to trusted IPs, and remember that Docker’s published ports bypass UFW. Add Fail2ban for repeated failures, enable unattended security upgrades, bind internal services to localhost, and cover IPv6 as well as IPv4.