Protecting My Server with Fail2ban

I was going through my Microbin logs the other day and found this:

185.177.72.50 "GET /wp-config.php.bak HTTP/1.0" 200
185.177.72.50 "GET /env HTTP/1.0" 200
185.177.72.50 "GET /debug.log HTTP/1.0" 200
185.177.72.50 "GET /admin HTTP/1.0" 302

Dozens of requests, all within seconds, all from the same IP. Classic scanner behavior just probing for anything left exposed. My server had been up for maybe 20 minutes before this started.

That's when I decided to set up Fail2ban properly instead of leaving it to Nginx rate limiting alone.

What Fail2ban actually does

It's simpler than it sounds. Fail2ban watches your log files and when it sees an IP doing something suspicious too many failed SSH logins, too many 429s from Nginx, too many bad auth attempts it adds an iptables rule to drop all traffic from that IP.

No dashboard. No agent. No background service eating 200MB of RAM. Just a Python daemon reading log files and writing firewall rules. On my Lightsail instance it sits at around 13MB.

It works through "jails" each jail watches a specific service with its own threshold and ban duration. SSH gets tighter rules than everything else because it's the most attacked.

Installation

sudo apt install fail2ban -y
sudo systemctl enable --now fail2ban

That's it for installation. The default config protects SSH out of the box but I wanted more.

My config

Create /etc/fail2ban/jail.local never edit jail.conf directly, package updates will overwrite it:

[DEFAULT]
bantime  = 1h
findtime = 10m
maxretry = 5
ignoreip = 127.0.0.1/8 YOUR_HOME_IP YOUR_SERVER_IP

bantime.increment = true
bantime.multiplier = 2
bantime.maxtime = 1w

[sshd]
enabled  = true
maxretry = 3
bantime  = 24h

[nginx-http-auth]
enabled  = true
logpath  = /var/log/nginx/error.log
maxretry = 3

[nginx-req-limit]
enabled  = true
filter   = nginx-req-limit
logpath  = /var/log/nginx/error.log
maxretry = 5

A few things worth explaining here.

bantime.increment = true means repeat offenders get progressively longer bans. First ban is 1 hour, second is 2 hours, third is 4 hours, up to a week maximum. Someone who keeps coming back gets treated accordingly.

ignoreip is important. Add your home IP and your server's own IP. Without this you can accidentally lock yourself out which is not a fun situation when you're 100km from the datacenter.

The [nginx-req-limit] jail works together with whatever rate limiting you have in Nginx. When Nginx rejects a request with a 429, it logs it as an error. Fail2ban sees enough of those from one IP and bans them at the firewall level so they stop hitting Nginx entirely.

The nginx-req-limit filter

This filter doesn't ship with Fail2ban by default, you have to create it:

sudo nano /etc/fail2ban/filter.d/nginx-req-limit.conf
[Definition]
failregex = limiting requests, excess:.* by zone .*, client: <HOST>
ignoreregex =

Then restart:

sudo systemctl restart fail2ban

Checking it works

sudo fail2ban-client status

Should show something like:

Status
|- Number of jail: 3
`- Jail list: nginx-http-auth, nginx-req-limit, sshd

Check a specific jail:

sudo fail2ban-client status sshd

This shows currently banned IPs and total failures. On a fresh server you'll see bans within hours there are bots constantly scanning the entire IPv4 space.

To manually unban an IP (if you accidentally lock yourself out):

sudo fail2ban-client set sshd unbanip YOUR_IP

What it doesn't protect against

Fail2ban won't save you from a real distributed DDoS if 10,000 different IPs each send one request, there's no pattern to detect. For that you need something upstream like Cloudflare sitting in front of your server.

What it does well: single-IP brute force, credential stuffing from one source, aggressive scanners like the one I found in my logs. For a personal homelab server that's the realistic threat model most of the time.

The scanner from my logs

That 185.177.72.50 that was scanning my server with Fail2ban running, it would have been banned after hitting the Nginx rate limiter 5 times. Instead of dozens of requests getting logged, it would have been blocked at the firewall after the first few.

Small thing. But multiply that across SSH, Nginx auth, and everything else running on the server and it adds up.

The config above is what I'm running now on my Lightsail instance alongside Nginx rate limiting. Both together handle the realistic day-to-day noise without touching the server's RAM budget meaningfully.

+++