Using Fail2Ban to insta-block malicious hits
Posted on Tue 07 April 2020 in Tech
Okay so a few days ago I posted about adopting Caddy.
Of course, anyone who hosts anything knows that accessible services on the internet will start getting hit by scanners within literal moments of being accessible. On the very first night of having my site live on the new VPS, Logwatch informed me of a bunch of attempts to access wp-admin.php. Sigh.
Well, this entire site is built with my favourite static site generator Pelican. That means I don't have any PHP on my site whatsoever. Why not just... block anyone that tries to open a PHP page at ericlight.com? Fail2Ban to the rescue.
Two things we need: A Fail2Ban filter, and a Fail2Ban jail.
nano /etc/fail2ban/filter.d/caddy-php.conf (note, the name of this file must be the same as the identifier in your
[Definition] failregex = ^<HOST>.*\.php(\s|\?.*)HTTP.*$ ignoreregex =
... that will catch anything in your logs such as:
nn.nn.nn.nn - - [07/Apr/2020:23:20:39 +1200] "GET /wp-admin.php HTTP/2.0" 404 0 nn.nn.nn.nn - - [06/Apr/2020:10:12:24 +1200] "GET /index.php?s=index/%5Cthink%5Capp/invokefunction&function=call_user_func_array&vars%5B0%5D=phpinfo&vars%5B1%5D%5B%5D=1 HTTP/1.1" 404 0 nn.nn.nn.nn - - [06/Apr/2020:12:51:31 +1200] "GET /wp-login.php HTTP/1.1" 404 0
[caddy-php] port = http,https logpath = /var/log/caddy/access.log enabled = true maxretry = 1
Note I used
maxretry = 1 there. This means if an IP triggers that filter a single time, they'll be blocked immediately... So you probably don't want to do this just willy-nilly.
With those two parts done (and the obligatory
service fail2ban reload, of course), you should find that your web server immediately bans any host that tries to load a .php file from your website!
# tail -f /var/log/caddy/access.log /var/log/fail2ban.log ==> /var/log/caddy/access.log <== 1xx.1xx.1xx.11 - - [07/Apr/2020:23:25:55 +1200] "GET /arse.php HTTP/1.1" 404 0 1xx.1xx.1xx.6 - - [07/Apr/2020:23:25:57 +1200] "GET /arse.php?1234 HTTP/1.1" 404 0 1xx.2xx.6x.1xx - - [07/Apr/2020:23:26:03 +1200] "GET /blargh.php HTTP/2.0" 404 0 ==> /var/log/fail2ban.log <== 2020-04-07 23:25:55,482 fail2ban.filter : INFO [caddy-php] Found 1xx.1xx.1xx.11 - 2020-04-07 23:25:55 2020-04-07 23:25:55,551 fail2ban.actions : NOTICE [caddy-php] Ban 1xx.1xx.1xx.11 2020-04-07 23:25:58,309 fail2ban.filter : INFO [caddy-php] Found 1xx.1xx.1xx.6 - 2020-04-07 23:25:57 2020-04-07 23:25:58,782 fail2ban.actions : NOTICE [caddy-php] Ban 1xx.1xx.1xx.6 2020-04-07 23:26:03,627 fail2ban.filter : INFO [caddy-php] Found 1xx.2xx.6x.1xx - 2020-04-07 23:26:03 2020-04-07 23:26:04,010 fail2ban.actions : NOTICE [caddy-php] Ban 1xx.2xx.6x.1xx
Great success! You can use the
banTime directive to adjust how long these blocks should last for - I've got mine set up to block for an hour.
Eh I realised that my RegEx missed these lovely things:
[ip redacted] - - [08/Apr/2020:16:14:10 +1200] "GET /wp-config.php.new HTTP/1.1" 404 0 [ip redacted] - - [08/Apr/2020:16:14:14 +1200] "GET /wp-config.php.old HTTP/1.1" 404 0 [ip redacted] - - [08/Apr/2020:16:14:17 +1200] "GET /wp-config.php.bak HTTP/1.1" 404 0 [ip redacted] - - [08/Apr/2020:16:14:19 +1200] "GET /wp-config.php.backup HTTP/1.1" 404 0 [ip redacted] - - [08/Apr/2020:16:14:22 +1200] "GET /wp-config.php.save HTTP/1.1" 404 0
... so yeah I just changed it to drop anything with
.php in the URL. I'll try to remember not to post any articles with .php in the URL lol.
Thanks to Phage and Xyphoid for the help in fine-tuning my rusty RegEx!