Securing Your VPS with UFW Firewall and Fail2Ban

Securing Your VPS with UFW Firewall and Fail2Ban

Your VPS is your gateway to self-hosting, but it's also exposed to the internet 24/7. Within minutes of bringing a new server online, automated bots start probing for weak SSH credentials, open ports, and known vulnerabilities. I learned this the hard way when I watched my access logs fill with thousands of failed login attempts in a single day. That's when I realized that a firewall and intrusion detection aren't optional—they're essential. In this guide, I'll show you how to layer UFW and Fail2Ban to create a practical, effective defense that actually works without being painful to maintain.

Why UFW and Fail2Ban Matter

UFW (Uncomplicated Firewall) is a frontend for Linux's iptables that makes it trivial to define what traffic your server accepts. Fail2Ban watches your logs in real-time and automatically blocks IP addresses that show malicious behavior—like repeated failed SSH logins.

Together, they create two layers of defense: UFW is your first wall, blocking everything except what you explicitly allow. Fail2Ban is your guard, watching for patterns of abuse and escalating the response automatically.

If you're running a budget VPS—and you should be (providers like RackNerd offer solid 1GB/2GB options for around $40/year)—you can't afford to waste resources on heavyweight intrusion detection systems. UFW and Fail2Ban run on minimal hardware and integrate perfectly with Docker-based homelab setups.

Installing UFW and Fail2Ban

First, update your system and install both packages. I prefer Ubuntu/Debian for this reason alone—the ecosystem is mature and well-documented.

sudo apt update && sudo apt upgrade -y
sudo apt install ufw fail2ban -y

Verify the installation:

sudo ufw version
sudo fail2ban-client --version
Tip: Before enabling UFW, make absolutely sure you allow SSH or you'll lock yourself out. I recommend opening a second terminal connection before proceeding.

Configuring UFW: Your Perimeter Defense

UFW works on a simple principle: deny everything by default, allow only what you need. Let's start by setting the defaults and allowing SSH.

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

Those last two rules are for HTTP and HTTPS—essential if you're running a web service. If you're running Docker containers, you'll need to open specific ports where your services listen. For example, if you're running Nextcloud on port 8080:

sudo ufw allow 8080/tcp

Now enable the firewall:

sudo ufw enable

Check the status to confirm your rules loaded correctly:

sudo ufw status verbose

You should see output like:

Status: active

     To                         Action      From
     --                         ------      ----
22/tcp                     ALLOW       Anywhere
80/tcp                     ALLOW       Anywhere
443/tcp                     ALLOW       Anywhere

That's the foundation. Everything else gets dropped at the firewall level—no need to even reach your application.

Configuring Fail2Ban: Your Active Defense

Fail2Ban works by parsing log files for patterns. When it finds a pattern (like 5 failed SSH logins in 10 minutes), it temporarily bans that IP. Let's set it up.

The main configuration file is at /etc/fail2ban/jail.conf, but we customize it in /etc/fail2ban/jail.local so updates don't overwrite our settings:

sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local

Now edit the local config:

sudo nano /etc/fail2ban/jail.local

Find and modify these core settings (use Ctrl+W to search in nano):

[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 5
destemail = [email protected]
sendername = Fail2Ban
action = %(action_mwl)s

[sshd]
enabled = true
port = ssh
logpath = %(sshd_log)s
maxretry = 4

Let me explain these values because they matter:

Watch out: If you use email notifications, Fail2Ban needs sendmail or postfix installed. On minimal VPS installs, this often isn't present. Either install it or change action to just %(action)s (ban only, no email).

Save and exit (Ctrl+O, Enter, Ctrl+X in nano). Now restart Fail2Ban:

sudo systemctl restart fail2ban

Verify it's running and monitoring SSH:

sudo fail2ban-client status
sudo fail2ban-client status sshd

You'll see output like:

Status for the jail sshd:
|- Filter
|  |- Currently failed:	0
|  |- Total failed:	12
|  `- File list:	/var/log/auth.log
`- Actions
   |- Currently banned:	2
   |- Total banned:	3
   `- IP list:	192.0.2.1 198.51.100.5

This tells you how many IPs are currently banned and total attempts blocked. That's real-time protection working.

Adding Custom Jails for Docker Services

If you're running self-hosted apps like Nextcloud, Vaultwarden, or Jellyfin in Docker, they generate their own logs. You can create custom Fail2Ban jails to protect them too.

Create a custom filter file:

sudo nano /etc/fail2ban/filter.d/nextcloud.conf

Add this filter for Nextcloud failed logins:

[Definition]
failregex = ^.*("app":"authentication","message":"Login failed:.*","userAgent":""
ignoreregex =

Then create a jail for it:

sudo nano /etc/fail2ban/jail.d/nextcloud.local
[nextcloud]
enabled = true
port = http,https
filter = nextcloud
logpath = /var/lib/docker/containers/*/log-json.log
maxretry = 5
findtime = 600
bantime = 3600

Restart Fail2Ban to load the new jail:

sudo systemctl restart fail2ban
sudo fail2ban-client status nextcloud

Now Nextcloud brute-force attempts are automatically blocked too.

Testing Your Setup

You can test that Fail2Ban is working by intentionally triggering bans. Create a test with a non-existent user:

for i in {1..6}; do ssh baduser@localhost; done

This will fail 6 times (exceeding maxretry of 5) and trigger a ban. Check the status:

sudo fail2ban-client status sshd

You should see your local IP in the banned list. Test that the ban works:

ssh your-username@your-server

It should timeout or reject. Wait a minute, then try again—your legit user will connect because we only banned that specific source. Unban yourself if needed:

sudo fail2ban-client set sshd unbanip 127.0.0.1

Monitoring and Maintenance

Check Fail2Ban logs regularly to understand attack patterns:

sudo tail -50 /var/log/fail2ban.log

Look for which IPs are being blocked most frequently. If you see legitimate traffic being banned (happens occasionally with shared office networks), you can whitelist it:

sudo nano /etc/fail2ban/jail.local

Add to the [DEFAULT] section:

ignoreip = 127.0.0.1/8 ::1 192.0.2.0/24

Replace 192.0.2.0/24 with your actual safe subnet. Restart Fail2Ban after changes.

Integration with Your Homelab

This setup works seamlessly with Docker-based homelabs. UFW ports map directly to your container ports, and Fail2Ban can monitor logs from any service. When you add a new self-hosted app, you just:

  1. Expose the service on a specific port in Docker Compose
  2. Allow that port through UFW with sudo ufw allow PORT/tcp
  3. Optionally create a custom Fail2Ban jail if the service has meaningful logs

For a $40/year budget VPS running Nextcloud, Vaultwarden, and Jellyfin, this two-layer approach is bulletproof. I've monitored my own servers for months with UFW+Fail2Ban, and my attack surface dropped by ~95% within the first week.

Next Steps

Now that your firewall and intrusion detection are hardened, consider adding SSH key authentication (disable password logins entirely) and running your services behind a reverse proxy like Caddy or Traefik for an additional layer of security. Your VPS is a serious tool—treat it that way from day one.

Discussion