The Slow Gulls

Ideas, texts and images between sky, earth and sea

social: [ mastodon logo mastodon | github logo github | sourcehut logo sourcehut | radicle logo radicle ]

lang: [ fr | en ]

A Fail2Ban configuration to block bots hitting nginx

2026-05-20

Every day, bots scan for servers to attack. We'll configure an scalable firewall to stop them from scanning us.

If you have a server exposed to the internet, you probably have a web server running on it. If you look at its access logs, you’ll likely see attempted accesses to /PHPMyAdmin/, /.env, and other attempts to exploit potential vulnerabilities.

If you have a server exposed to the internet, you probably have an SSH server running on it. If, like me, you don’t have a fixed IP address, you probably don’t have a firewall rule blocking connections from any IP outside a restricted set of known addresses either.

In that case, you’re probably already using Fail2Ban. If you aren’t, stop reading my site and go find a tutorial to install and configure it.

Example logs you might see on an HTTP server:

20.104.227.76 - - [19/May/2026:17:36:26 +0000] "GET /wp-content/plugins/hellopress/wp_filemanager.php HTTP/1.1" 404 162 "-" "-"
20.104.227.76 - - [19/May/2026:17:36:26 +0000] "GET /r.php HTTP/1.1" 404 162 "-" "-"
20.104.227.76 - - [19/May/2026:17:36:26 +0000] "GET /ight.php HTTP/1.1" 404 162 "-" "-"
20.104.227.76 - - [19/May/2026:17:36:26 +0000] "GET /wpns.php HTTP/1.1" 404 162 "-" "-"
20.104.227.76 - - [19/May/2026:17:36:26 +0000] "GET /er.php HTTP/1.1" 404 162 "-" "-"
20.104.227.76 - - [19/May/2026:17:36:26 +0000] "GET /sql.php HTTP/1.1" 404 162 "-" "-"
20.104.227.76 - - [19/May/2026:17:36:27 +0000] "GET /lang/es.php HTTP/1.1" 404 162 "-" "-"
20.104.227.76 - - [19/May/2026:17:36:27 +0000] "GET /getir.php HTTP/1.1" 404 162 "-" "-"
20.104.227.76 - - [19/May/2026:17:36:27 +0000] "GET /core/init.php HTTP/1.1" 404 162 "-" "-"
20.104.227.76 - - [19/May/2026:17:36:27 +0000] "GET /aa.php HTTP/1.1" 404 162 "-" "-"

I don’t want my server to keep accepting this kind of attempt: it burns sockets, it burns stat() calls on the filesystem, and there are plenty of other reasons I don’t want it.

So I created a Fail2Ban filter to ban this sort of behaviour.

# /etc/fail2ban/filter.d/nginx-404.conf
[Definition]
failregex = ^<HOST> - - \[\.*\] \"[A-Z]+ \S+ HTTP/\d\.?\d?\" 4[0-9]{2} \d+ \"-\" \".*\"$

We then need to enable this filter in our jail.local:

# /etc/fail2ban/jail.local

# … other jail definitions …

[nginx-404]
enabled = true
port = http,https
filter = nginx-404
logpath  = %(nginx_access_log)s
           /var/log/nginx/*.vit.am.log
maxretry = 3
bantime = 3600
findtime = 600

# … other jail definitions …

NOTE: %(nginx_access_log)s is defined in one of the files included by jail.local and corresponds to a glob expression matching my nginx access log layout.

And that’s it! All that’s left is systemctl reload fail2ban.service and we’re good to go.

After a while you can verify that the filter is working as expected:

root@myserver:~# fail2ban-client status nginx-404
Status for the jail: nginx-404
|- Filter
|  |- Currently failed: 0
|  |- Total failed:     19
|  `- File list:        /var/log/nginx/goelands.vit.am.access.log /var/log/nginx/goelands.vit.am.log (truncated list)
`- Actions
   |- Currently banned: 2
   |- Total banned:     2
   `- Banned IP list:   20.104.227.76 128.140.125.52

It’s worth noting there are several things that can be improved:

  1. the regex used to match log lines is CPU-expensive due to the use of .*, which requires far more cycles than a stricter match — I should revisit the different possible log line formats and see whether I can reduce the number of cycles needed for evaluation; EDIT1: fixed in the configuration listing above, dropping from 8980 steps to 516 with the new one (^<HOST> - \S+ \[\S+ [+-]\d{4}\] "\S+ \S+ \S+" 4[0-9]{2} \d+ "[^"]*" "[^"]*"$). EDIT2: The new regex doesn’t work for an unknown reason (it is valid (and matches) in every regex debugging tool i tried), so i’m reverting to a barely more efficient than the original one: ^<HOST> - - \[\.*\] \"[A-Z]+ \S+ HTTP/\d\.?\d?\" 4[0-9]{2} \d+ \"-\" \".*\"$.
  2. a POST will probably not return a 404 (Not Found), but perhaps a 405 (Method Not Allowed) or even a 400 (Bad Request) — it may be worth matching on HTTP codes 40[0-9]; → likewise, changed to ban all HTTP 4xx. Be careful not to enable this on a service with a login! Or at least exclude 401s.
  3. you need to make sure that the sites in question have few broken internal links, as a legitimate user could end up banned simply for following a dead link.

If you have any comments or feedback, as always find me at @ololduck@fosstodon.org.