Hardening Your VPS: SSH Key Authentication, Fail2Ban, and UFW Firewall Setup
We earn commissions when you shop through the links on this page, at no additional cost to you. Learn more.
The moment you spin up a new VPS, bots start knocking on port 22. I've watched auth logs on a fresh Hetzner droplet and seen thousands of failed login attempts within the first hour — no exaggeration. The good news is that three straightforward tools — SSH key authentication, Fail2Ban, and UFW — will shut down the vast majority of those attacks, and you can have all three configured in under thirty minutes.
This tutorial walks through the complete hardening process on Ubuntu 22.04 or 24.04 (Debian works identically for everything here). I'll cover generating and deploying SSH keys, locking down the SSH daemon config, setting up UFW with sensible defaults, and configuring Fail2Ban to automatically ban repeat offenders. By the end your server will be significantly more resistant to both automated scans and targeted brute-force attempts.
If you're looking for a reliable VPS to practice on, DigitalOcean Droplets are my go-to for tutorials like this — predictable pricing, fast SSD storage, and a clean Ubuntu image that's ready in under a minute.
Step 1: Generate an SSH Key Pair on Your Local Machine
If you're still logging in with a password, that needs to change today. SSH key pairs replace guessable passwords with cryptographic authentication. Generate a new Ed25519 key pair on your local workstation — Ed25519 is faster and more secure than the older RSA 2048 keys many guides still recommend:
ssh-keygen -t ed25519 -C "[email protected]" -f ~/.ssh/vps_ed25519
When prompted for a passphrase, use one. It adds a second factor — even if your private key file is somehow stolen, the attacker still can't use it without the passphrase. Now copy the public key to your VPS. If you still have password access, ssh-copy-id is the quickest route:
ssh-copy-id -i ~/.ssh/vps_ed25519.pub -p 22 youruser@your-server-ip
If ssh-copy-id isn't available (Windows users, I see you), you can manually append the public key. On the server, run:
mkdir -p ~/.ssh
chmod 700 ~/.ssh
echo "paste-your-public-key-here" >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
Test the key login before touching anything else: ssh -i ~/.ssh/vps_ed25519 youruser@your-server-ip. Confirm it works. If it doesn't, debug now — you don't want to lock yourself out in the next step.
Step 2: Harden the SSH Daemon Configuration
Now that key authentication works, disable password logins and tighten the SSH daemon. Open the config file:
sudo nano /etc/ssh/sshd_config
Find and set the following directives. If a line is commented out with #, uncomment it and change the value:
# Disable password authentication entirely
PasswordAuthentication no
# Disable challenge-response (keyboard-interactive) auth
KbdInteractiveAuthentication no
# Disable root login over SSH
PermitRootLogin no
# Only allow specific users (replace with your actual username)
AllowUsers youruser
# Use only modern key exchange algorithms
Protocol 2
# Reduce the authentication timeout window
LoginGraceTime 30
# Limit concurrent unauthenticated connections
MaxStartups 3:50:10
# Change the default SSH port (optional but reduces noise)
Port 2222
Changing the port from 22 to something like 2222 or 4822 isn't real security, but it does eliminate the vast majority of automated scanners and makes your auth logs dramatically quieter. I find it worth the minor inconvenience of specifying -p 2222 when connecting.
After editing, reload the SSH daemon without closing your current session (critical safety step):
sudo systemctl reload sshd
Open a second terminal and test your connection with the new settings before closing the first. If it works, you're safe. If not, your first session is still open and you can fix the config.
Step 3: Configure UFW Firewall
UFW (Uncomplicated Firewall) wraps iptables in a sane interface. It's installed by default on Ubuntu but not enabled. Here's my standard setup for a VPS running web services:
# Start with a clean slate — deny everything incoming, allow everything outgoing
sudo ufw default deny incoming
sudo ufw default allow outgoing
# Allow SSH on your chosen port (adjust if you changed it from 22)
sudo ufw allow 2222/tcp comment 'SSH'
# Allow HTTP and HTTPS for web services
sudo ufw allow 80/tcp comment 'HTTP'
sudo ufw allow 443/tcp comment 'HTTPS'
# If you're running Portainer web UI locally only, don't open 9000 to the world
# Only open ports you actually need
# Enable UFW
sudo ufw enable
# Verify the rules look correct
sudo ufw status verbose
The comment flag is underused — it makes rules readable months later when you've forgotten why port 8096 is open (that's Jellyfin, by the way). I always add comments. Rate limiting is also built into UFW and is worth enabling on your SSH port as a complement to Fail2Ban:
sudo ufw limit 2222/tcp comment 'SSH rate limit'
This blocks any IP that attempts more than six connections in thirty seconds, which catches some brute-force attempts even before Fail2Ban gets involved.
-p in Docker will be reachable from the internet regardless of your UFW rules. To fix this, either use Docker's internal networks and proxy through Caddy or Traefik, or configure Docker to respect UFW by editing /etc/docker/daemon.json and setting "iptables": false — though the latter requires manual iptables management.Step 4: Install and Configure Fail2Ban
UFW rate limiting is a blunt instrument. Fail2Ban is smarter: it watches your log files in real time and automatically bans IPs that show suspicious patterns — too many failed SSH logins, bad HTTP auth attempts on your self-hosted apps, and more.
sudo apt update
sudo apt install fail2ban -y
Fail2Ban's default config lives in /etc/fail2ban/jail.conf, but you should never edit that file directly — it gets overwritten on updates. Instead, create a local override file:
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
sudo nano /etc/fail2ban/jail.local
Find the [DEFAULT] section and adjust these values. Then find or create the [sshd] jail section:
[DEFAULT]
# Ban IPs for 1 hour
bantime = 3600
# Look back 10 minutes for failures
findtime = 600
# Allow 3 attempts before banning
maxretry = 3
# Your own IP(s) — never get banned yourself
ignoreip = 127.0.0.1/8 ::1
[sshd]
enabled = true
port = 2222
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 86400
I set a longer bantime of 86400 seconds (24 hours) on the SSH jail specifically. Bots move on quickly; a 1-hour ban barely slows them down. For SSH I want them gone for a full day. Enable and start Fail2Ban:
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
# Check that the SSH jail is active
sudo fail2ban-client status sshd
You'll see the number of currently banned IPs and total bans. On a busy server, after 24 hours this number is usually in the hundreds. To manually unban an IP (useful if you accidentally ban yourself):
sudo fail2ban-client set sshd unbanip THE.IP.ADDRESS.HERE
Step 5: Ongoing Monitoring
Once everything is running, peek at your auth log occasionally to confirm the defenses are working:
# Watch live SSH activity
sudo tail -f /var/log/auth.log
# See current Fail2Ban bans across all jails
sudo fail2ban-client status
# Check UFW logs
sudo tail -f /var/log/ufw.log
I also recommend setting up Uptime Kuma and pairing it with Grafana for longer-term visibility into failed auth trends. It's satisfying to watch the ban count climb and know your server is shrugging off thousands of attacks a day.
If you need a solid VPS to put all of this into practice, DigitalOcean Droplets offer dependable uptime with a 99.99% SLA and predictable monthly pricing — a great foundation for a properly hardened self-hosted environment.
Wrapping Up
With SSH keys deployed, password authentication disabled, UFW blocking everything except your explicitly allowed ports, and Fail2Ban actively banning brute-force attempts, your VPS is in far better shape than the average internet-exposed server. This baseline takes under thirty minutes to set up and dramatically reduces your attack surface.
Your next steps: consider adding Authelia in front of your web-exposed services for two-factor authentication, and look at WireGuard if you want to stop exposing SSH to the public internet entirely — tunnel your admin access through VPN and your attack surface shrinks to almost nothing.
Discussion