Fail2ban with Cloudflare

💡
This tutorial is part of the How to: Fail2Ban tutorials series, you may check the main post to know what Fail2ban is, and how Fail2Ban works.

In this case, I want to protect an app in Docker container with Fail2ban which uses Cloudflare CDN.

Prerequisites

This tutorial was adapted from my other fail2ban guide (Protect Wordpress Login in Docker container with Fail2Ban) and enables Cloudflare CDN "the orange cloud" on the subdomain. So, in order to follow these steps (Step 1, Step 2 and so on), you may adapt your jail config or deploy the same WP container first:

  • Install Docker Engine (docs)
  • A docker-compose wordpress-mysql (gists)
  • Nginx proxy, if you deployed WP on a different port
  • Install curl package with apt install curl
Protect Wordpress Login in Docker container with Fail2Ban
Protect Wordpress Login in Docker container with Fail2Ban - compose.yaml

Step 1: Add a New Fail2ban Jail

Edit jail.local file:

sudo nano /etc/fail2ban/jail.local

Add a new jail named "cf-wplogin":

[cf-wplogin]

enabled = true
port = http,https
filter = wplogin
chain = DOCKER-USER
logpath = /var/lib/docker/containers/*/*-json.log
banaction = cloudflare-apiv4
            iptables-allports

Step 2: Create a Custom Fail2ban Filter

Create a new filter file : nano /etc/fail2ban/filter.d/wplogin.conf

[Definition]
failregex = {"log":"<HOST> -.*POST.*wp-login.php.*
ignoreregex =

Step 3: Create a Fail2ban action

Create a new action file: nano /etc/fail2ban/action.d/cloudflare-apiv4.conf

#
# Author: Gilbn from https://technicalramblings.com
# Adapted Source: https://github.com/fail2ban/fail2ban/blob/master/config/action.d/cloudflare.conf and https://guides.wp-bullet.com/integrate-fail2ban-cloudflare-api-v4-guide/
#
# To get your Cloudflare API key: https://dash.cloudflare.com/profile use the Global API Key
#

[Definition]

# Option:  actionstart
# Notes.:  command executed once at the start of Fail2Ban.
# Values:  CMD
#
actionstart =

# Option:  actionstop
# Notes.:  command executed once at the end of Fail2Ban
# Values:  CMD
#
actionstop =

# Option:  actioncheck
# Notes.:  command executed once before each actionban command
# Values:  CMD
#
actioncheck =

# Option:  actionban
# Notes.:  command executed when banning an IP. Take care that the
#          command is executed with Fail2Ban user rights.
# Tags:      IP address
#            number of failures
#            unix timestamp of the ban time
# Values:  CMD

actionban = curl -s -X POST "https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules" \
            -H "X-Auth-Email: <cfuser>" \
            -H "X-Auth-Key: <cftoken>" \
            -H "Content-Type: application/json" \
            --data '{"mode":"block","configuration":{"target":"ip","value":"<ip>"},"notes":"Fail2ban <name>"}'

# Option:  actionunban
# Notes.:  command executed when unbanning an IP. Take care that the
#          command is executed with Fail2Ban user rights.
# Tags:      IP address
#            number of failures
#            unix timestamp of the ban time
# Values:  CMD
#

actionunban = curl -s -X DELETE "https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules/$( \
              curl -s -X GET "https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules?mode=block&configuration_target=ip&configuration_value=<ip>&page=1&per_page=1&match=all" \
             -H "X-Auth-Email: <cfuser>" \
             -H "X-Auth-Key: <cftoken>" \
             -H "Content-Type: application/json" | awk -F"[,:}]" '{for(i=1;i<=NF;i++){if($i~/'id'\042/){print $(i+1);}}}' | tr -d '"' | sed -e 's/^[ \t]*//' | head -n 1)" \
             -H "X-Auth-Email: <cfuser>" \
             -H "X-Auth-Key: <cftoken>" \
             -H "Content-Type: application/json"

[Init]

# Name of the jail in your jail.local file. default = [jail name]
name = default

# Option: cfuser
# Notes.: Replaces <cfuser> in actionban and actionunban with cfuser value below
# Values: Your CloudFlare user account

cfuser = [email protected]

# Option: cftoken (Global API Key)
# Notes.: Replaces <cftoken> in actionban and actionunban with cftoken value below
# Values: Your CloudFlare API key 
cftoken = YOUR-API-KEY
This action script taken from technicalramblings.com

As you can notice, the actionban and actionunban is curl command to cloudflare restapi.

You can get your cloudflare api key from your profile page. Use the "Global Api Key"

https://ns1.my.id/unggah/2023/03/cf-global-apikey.png

After adding a new jail, filter, and action, Fail2ban should be restarted. Restart fail2ban with:

sudo systemctl stop fail2ban
sudo systemctl start fail2ban

Step 4: Nginx

As I mentioned earlier, these steps adapted from (Protect Wordpress Login in Docker container with Fail2Ban) which a wordpress container running on 127.0.0.1 port 8080 and proxied to 80 with NGINX.

Next step is configuring nginx so that it won't just ban the Cloudflare CDN IP but the actual IP of the visitor. Create a file named cf-realip.conf in /etc/nginx/conf.d directory and add the following:

set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 104.16.0.0/12;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 131.0.72.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 2400:cb00::/32;
set_real_ip_from 2606:4700::/32;
set_real_ip_from 2803:f800::/32;
set_real_ip_from 2405:b500::/32;
set_real_ip_from 2405:8100::/32;
set_real_ip_from 2c0f:f248::/32;
set_real_ip_from 2a06:98c0::/29;
/etc/nginx/conf.d/cf-realip.conf

These IPs are Cloudflare's IPs, you can get the list at https://www.cloudflare.com/ips

And edit your main nginx.conf:

nano /etc/nginx/nginx.conf

Add the following code:

##
# CF Real IP
##
include /etc/nginx/conf.d/cf-realip.conf;
real_ip_header X-Forwarded-For;

restart your nginx with:

service nginx restart

Step 5: Let's try it out!

Check the jail status with the following command:

fail2ban-client status cf-wplogin
Output:
root@server:~# fail2ban-client status cf-wplogin
Status for the jail: cf-wplogin
|- Filter
|  |- Currently failed: 0
|  |- Total failed:     0
|  `- File list:        /var/lib/docker/containers/599f2f26530ec5b26cbd5d542a3034ef6f54fdd78b32587ef7e33f8518e2493d/599f2f26530ec5b26cbd5d542a3034ef6f54fdd78b32587ef7e33f8518e2493d-json.log
 /var/lib/docker/containers/d1dbe55aed91c1f2e4095eb05a5a14dd0750e13b88e49b9786623e5903c22dc6/d1dbe55aed91c1f2e4095eb05a5a14dd0750e13b88e49b9786623e5903c22dc6-json.log
`- Actions
   |- Currently banned: 0
   |- Total banned:     0
   `- Banned IP list:

And the last step, let's try creating an error to trigger fail2ban jail with open your wordpress login in your web browser:

https://ns1.my.id/unggah/2023/03/wordpress-docker-nginx-fail2ban.gif

If you are trying to access other subdomain, cloudflare will display this error 1006 Access Denied, because your IP has been banned entirely.

https://ns1.my.id/unggah/2023/03/cloudflare-waf-fail2ban2.jpg

List Blocked IP

To get a list of blocked IPs in your server, run the following command:

fail2ban-client status cf-wplogin
Output:
root@server:~# fail2ban-client status cf-wplogin
Status for the jail: cf-wplogin
|- Filter
|  |- Currently failed: 0
|  |- Total failed:     3
|  `- File list:        /var/lib/docker/containers/599f2f26530ec5b26cbd5d542a3034ef6f54fdd78b32587ef7e33f8518e2493d/599f2f26530ec5b26cbd5d542a3034ef6f54fdd78b32587ef7e33f8518e2493d-json.log
 /var/lib/docker/containers/d1dbe55aed91c1f2e4095eb05a5a14dd0750e13b88e49b9786623e5903c22dc6/d1dbe55aed91c1f2e4095eb05a5a14dd0750e13b88e49b9786623e5903c22dc6-json.log
`- Actions
   |- Currently banned: 1
   |- Total banned:     1
   `- Banned IP list: 192.168.0.6

You can also check the list of banned IP on Cloudflare dashboard -> Your domain -> Security -> WAF (Web Application Firewall) -> Tools

https://ns1.my.id/unggah/2023/03/cloudflare-waf-fail2ban.jpg
my IP successfully banned in Cloudflare

Unblock IP

To unblock banned IPs, run the following command:

fail2ban-client set cf-wplogin unbanip 192.168.0.6
‼️
You need to unban ip with fail2ban-client, and cannot unban IP directly on Cloudflare dashboard, otherwise, you still cannot access your site because the server still bans your ip ( look at banaction section in jail.local iptables-allports)

Reference:

Thank you for reading!