fail2ban

The need

One month passed, one Hetzner invoice was paid, and now I’m allowed to ask for opening up ports 25 and 465 in order to get the mail server online. The process itself is straight forward in the Hetzner interface and the ports get unlocked in minutes … and .. in minutes, my logs start getting these kind of entries:

$ sudo grep "SASL LOGIN auth" /var/log/mail.log
2026-02-25T18:41:18.158208+00:00 mail postfix/smtpd[33067]: warning: unknown[103.81.170.109]: SASL LOGIN authentication failed: (reason unavailable), sasl_username=guest@chirila.me
2026-02-25T18:51:28.275339+00:00 mail postfix/smtpd[33115]: warning: unknown[178.16.55.50]: SASL LOGIN authentication failed: (reason unavailable), sasl_username=willies
2026-02-25T18:54:09.032736+00:00 mail postfix/smtpd[33126]: warning: unknown[103.81.170.109]: SASL LOGIN authentication failed: (reason unavailable), sasl_username=office@chirila.me
...

Like … just turning up and all kind of IPs are trying to log in. Now that I’m looking a bit better at it, looks like Nginx logs also have some funny entries:

$ sudo grep 404 /var/log/nginx/andrei.chirila.me.access.log
172.94.9.253 - - [25/Feb/2026:13:54:58 +0000] "GET /.git/config HTTP/1.1" 404 162 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
172.94.9.253 - - [25/Feb/2026:13:54:58 +0000] "GET /admin/.git/config HTTP/1.1" 404 162 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
172.94.9.253 - - [25/Feb/2026:13:54:58 +0000] "GET /private/.git/config HTTP/1.1" 404 162 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
...

And going further … quickly checking ssh logs, it’s also full of … surprises:

$ sudo grep -E "sshd\[.*(Failed|Invalid)" /var/log/auth.log | tail
2026-02-25T19:27:08.240110+00:00 mail sshd[34002]: Failed password for root from 188.166.84.53 port 54922 ssh2
2026-02-25T19:27:18.636668+00:00 mail sshd[34008]: Invalid user admin from 167.71.140.121 port 39598
2026-02-25T19:27:20.183401+00:00 mail sshd[34008]: Failed password for invalid user admin from 167.71.140.121 port 39598 ssh2
2026-02-25T19:27:53.520626+00:00 mail sshd[34018]: Failed password for root from 188.166.84.53 port 59718 ssh2
...

The solution

At this point I’m just wondering: isn’t there something that can extract from the logs the IP addresses and somehow block them … and I find AbuseIPDB. Having a look, it seems like I can use a straight off the shelf solution: fail2ban - even open source :D

Straight on installation with:

$ sudo apt install fail2ban

Then enabling with jail.local a couple more jails: nginx-botsearch, nginx-bad-request and postfix. This looks like:

[nginx-botsearch]
enabled = true
port = http,https
logpath = %(nginx_access_log)s

[nginx-bad-request]
enabled = true
port = http,https
logpath = %(nginx_access_log)s

[postfix]
enabled = true
filter = postfix[mode=aggressive]
maxretry = 3
bantime = 48h

[sshd]
enabled = true
action = %(action_)s
         %(action_abuseipdb)s[abuseipdb_category="18,22"]

The ssh logs start flowing straight away, and I’m also able to see them in the AbuseIPDB interface. But … only ssh reports were flowing in.

AbuseIPDB API usage

The knobs

Even on ssh, when trying to see if those IPs reported to AbuseIPDB have been banned … surprise: no entries.

# ufs status
... crickets banned IPs ...
OpenSSH                    ALLOW       Anywhere                  
Postfix                    ALLOW       Anywhere                  
Nginx Full                 ALLOW       Anywhere                  
Dovecot Secure IMAP        ALLOW       Anywhere 

Seems that the default action with which fail2ban is shipped is nftables, but Ubuntu, or at least 24.04 that I have running, manages the firewall with ufw.

Straight forward fix, in jail.local, is adding banaction = ufw. Once that’s in I can start seeing IPs being blocked:

$ ufw status | head
Status: active

To                         Action      From
--                         ------      ----
Anywhere                   REJECT      91.224.92.22               # by Fail2Ban after 2 attempts against sshd
Anywhere                   REJECT      45.148.10.152              # by Fail2Ban after 2 attempts against sshd

To postfix or to postfix@-

But still, that’s showing ufw blocking IPs only from the ssh jail while I have configured 4. Turns out that the default configuration on Ubuntu for postfix has journalmatch set to postfix.service. Trying to debug with journalctl one sees fast:

# journalctl -u postfix.service -p warning --since yesterday
-- No entries --

So the right value on this particular system is postfix@-.service. Forking it in /etc/fail2ban/filter.d/postfix.local fixed the issue.

# journalctl -u postfix@-.service -p warning --since yesterday
Mar 11 04:02:38 mail postfix/smtpd[313864]: warning: unknown[77.83.39.156]: SASL LOGIN authen>
Mar 11 04:02:44 mail postfix/smtpd[313864]: warning: unknown[77.83.39.156]: SASL LOGIN authen>

backend?

That pushed ufw status to show also IPs banned from Postfix logs, but there was no record from Nginx, even though the logs were still having plenty of entries that there were repeated attempts to access e.g. .php files.

The default fail2ban configuration comes with backend = systemd but … nginx doesn’t log accesses to systemd, but to files, and I have configured each domain to log to its own file. While the initial setup with logpath = %(ngingx_access_log)s was supposed to work, turns out that per jail one needs to set backend = auto to have the jail actually read the logs. With the customization in, reports also started flowing in.

AbuseIPDB API usage with categories

The end

This turned out a bit more complicated, setup-wise, than what I expected. On the other hand, I got to play with a couple of the fail2ban debugging tools and realized that in fact, debugging jail by jail, without rush, is totally doable.

In the process, I ended up also overriding some of the regexps for matching as some entries in the logs (e.g. postfix NMTL or nginx 400s) were not matched and also discovered that the initial version of the blog didn’t had robots.txt configured … ups.

Useful for next time I need to dive into this:

  • make your own overrides in /etc/fail2ban/filter.d/jail-name.local
  • fail2ban-regexp --print-all-missing sample-log-file "regexp" is your friend
  • fail2ban-client reload jail for quick iterations on the configuration, way faster than actually restarting the entire fail2ban.