One line appsec firewall for nginx w. fail2ban

After we saw that that a lot of traffic | ~25% | is coming from bots ( some kind of automated script or program ) who are trying to login to our hosted WordPress websites we added a simple one liner firewall that blocks them.

How? We will make a cron job to check the logs every minute and add the resulted ip’s to the fail2ban nginx-http-auth jail to easily manage them.

The line.

awk '{ if($9 ~ "login" && int($10) == 200 && $8 == "POST" && int($7) == 443) print $1, $6, $9, $10}' /var/log/nginx/access.log \
| sort -k1 -k2 | uniq -c | awk 'int($1) > 10 {print $2, $1}'

Explained

Supposed that the nginx/access.log file have the following log format

1.2.3.4 1.2.3.4 20/May/2020:12:28:26 +0300 iservit.ro iservit.ro 443 POST /wp-login.php 200

Setted up by the following log_format definition in nginx/server.conf

// /etc/nginx/nginx.conf
log_format compressed '$remote_addr $realip_remote_addr $time_local $server_name $host $server_port $request_method $uri $status';

The output will be all ip’s | $remote_addr | which match the following:

  • $9 ~ “login” | make a request to a ‘*login*’ path
  • int($10) == 200 | the returned status is 200 ( here you can change to 401 || 403 depends on your app/endpoint response )
  • $8 == “POST” | request method is “POST”
  • int($7) == 443 | request is mate through https
  • sort -k1 -k2 | sort by $remote_addr and $host
  • uniq -c | count total requests
  • awk ‘int($1) > 10 {print $2, $1}’ | print ip’s who exceed the “fail” threshold ( 10 in this case )

We got the bad ip’s so we need to block them.

Integrating Fail2Ban

If the service is not installed:

yum install -y fail2ban
service fail2ban start
service fail2ban status
cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local

and enable the nginx-http-auth ( and sshd ) jail in jail.local

/etc/fail2ban/jail.local
[sshd]
enabled = true
bantime = 3600000

[nginx-http-auth]
enabled = true
bantime = 3600000

Now if you try to ban an ip you can run fail2ban-client set nginx-http-auth banip 1.2.3.4 and check the chain status fail2ban-client status nginx-http-auth

Wrapping up

Supposing that the default sec. firewall rule will block more than 25 con/minute | iptables -A INPUT -p tcp --dport 80, 443 -m limit --limit 25/minute --limit-burst 100 -j ACCEPT | we set a cron job to scan the logs every minute. By default, fail2ban, scans log file every second but we consider this action not so critical so we keep it to once every minute.

  1. make the line executable and add ip’s to the fail2ban jail
  2. run once / minute
// nginx_fail2ban_firewall.sh
cat /var/log/nginx/access.log | awk '{ if($9 ~ "wp-login" && int($10) == 200 && $8 == "POST" && int($7) == 443) print $1, $6, $9, $10}' | \
sort -k1 -k2 | uniq -c | awk 'int($1) > 10 {print $2}' | \
grep -P '^\d{1,3}(\.\d{1,3}){3}\s' | \
while read line; do fail2ban-client set nginx-http-auth banip "$line"; done
// /etc/crontab
* * * * * root /root/nginx_fail2ban_firewall.sh >> /dev/null 2>&1

That’s all. Stay safe 🙂