CloudFlare is a pretty sweet free security, firewall and acceleration service that I use on all my WordPress sites. The old CloudFlare API is being retired shortly (November 2016, source). Since the CloudFlare v4 API is already live, I wanted to be prepared for the new API switch.
I use fail2ban to protect my wp-login with basic HTTP authentication (nginx, Apache) and D(D)oS protection from hacker bots that slam wp-login and xmlrpc.php. When you use CloudFlare, it will route all safe traffic to your web server unless you block certain IPs. Blocking IPs on my web servers alone is therefore not sufficient. Luckily fail2ban support custom actions so I wrote this integration with CloudFlare’s API v4 so CloudFlare won’t send unwanted traffic to my servers.
If you are using CloudFlare with WordPress or WooCommerce I highly recommend this sort of integration for additional security.
This fail2ban CloudFlare tutorial was tested on Ubuntu and Debian but should have no problems on CentOS or other Linux distributions.
How to Integrate fail2ban with CloudFlare API v4 Guide
Installation overview
- Test CloudFlare API v4 with cURL
- Create custom CloudFlare action for integration with fail2ban
I have only tested this on fail2ban 0.8.3 and 0.9.4, check your version with this command before proceeding.
sudo fail2ban-client -V
Test CloudFlare API v4
First install cURL
sudo apt-get update
sudo apt-get install curl -y
Go get your CloudFlare API key from here and replace the values below, we will ban IP 1.2.3.4 as a test.
This will be the fail2ban ban action.
curl -s -X POST "https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules" \
-H "X-Auth-Email: CloudFlare-username" \
-H "X-Auth-Key: CloudFlare-API-Key" \
-H "Content-Type: application/json" \
--data '{"mode":"block","configuration":{"target":"ip","value":"1.2.3.4"},"notes":"Fail2ban"}'
You should get this feedback showing the IP rule was added to CloudFlare, note the success: true
.
{"result":{"id":"8286c03a200db3b5d09bab0f414dc111","mode":"block","allowed_modes":["block","challenge","whitelist","js_challenge"],"status":"active","notes":"Fail2ban","scope":{"id":"b39e813030ef3ce72a82896683932a7d","email":"CloudFlare-username","type":"user"},"configuration":{"value":"1.2.3.4","target":"ip"},"created_on":"2016-09-08T14:36:36.770021Z","modified_on":"2016-09-08T14:36:36.770021Z"},"success":true,"errors":[],"messages":[]}
Now test deleting the CloudFlare firewall ban rule we just created. This will be used for the fail2ban unban action.
Note: This uses a nested cURL command, we need to get the rule ID in order to delete it.
curl -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=1.2.3.4&page=1&per_page=1&match=all" \
-H "X-Auth-Email: CloudFlare-username" \
-H "X-Auth-Key: CloudFlare-API-Key" \
-H "Content-Type: application/json" | awk -F"[,:}]" '{for(i=1;i<=NF;i++){if($i~/'id'\042/){print $(i+1)}}}' | tr -d '"' | head -n 1)" \
-H "X-Auth-Email: CloudFlare-username" \
-H "X-Auth-Key: CloudFlare-API-Key" \
-H "Content-Type: application/json"
You should get this feedback showing the IP rule was deleted from CloudFlare.
{"result":{"id":"8286c03a200db3b5d09bab0f414dc111"},"success":true,"errors":[],"messages":[]}
Create Custom CloudFlare Action for Integration with fail2ban
Create the CloudFlare fail2ban action, if it exists already you can back it up first
sudo mv /etc/fail2ban/action.d/cloudflare.conf /etc/fail2ban/action.d/cloudflare.conf.bak
sudo nano /etc/fail2ban/action.d/cloudflare.conf
Paste this configuration which we tested above using CloudFlare’s API v4 to ban an IP.
The unban action is the nested cURL command from above that deletes the ban when fail2ban’s bantime expires This will keep your CloudFlare IP blacklist clean.
Remember to put your CloudFlare username (your email) in cfuser
and your API key which can be found here in cftoken
.
#
# Author: Mike Andreasen from https://guides.wp-bullet.com
# Adapted Source: https://github.com/fail2ban/fail2ban/blob/master/config/action.d/cloudflare.conf
# Referenced from: https://www.normyee.net/blog/2012/02/02/adding-cloudflare-support-to-fail2ban by NORM YEE
#
# To get your Cloudflare API key: https://www.cloudflare.com/my-account
#
[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
#
Ctrl+X, Y and Enter to Save and Exit.
EDIT: Thanks to Vini’s research there are now updated ban and unban actions that solve the 400 bad request error
actionban = curl -s -o /dev/null -X POST -H 'X-Auth-Email: ' -H 'X-Auth-Key: ' \
-H 'Content-Type: application/json' -d '{ "mode": "block", "configuration": { "target": "ip", "value": "" } }' \
https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules
actionunban = curl -s -o /dev/null -X DELETE -H 'X-Auth-Email: ' -H 'X-Auth-Key: ' \
https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules/$(curl -s -X GET -H 'X-Auth-Email: ' -H 'X-Auth-Key: ' \
'https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules?mode=block&configuration_target=ip&configuration_value=&page=1&per_page=1' | tr -d '\n' | cut -d'"' -f6)
Now open up any fail2ban jail you want to integrate with CloudFlare’s API v4. I am using a pre-configured nginx-limit-req
jail for fail2ban to ban users who make too many requests to nginx.
sudo nano /etc/fail2ban/jail.d/nginx-limit-req.conf
Add an action line that includes our CloudFlare action, if you had no action line before the default is iptables-multiport
.
In some versions of fail2ban you need to call it actions
instead of action
otherwise the jail will not be activated and neither will the action.
[nginx-limit-req]
enabled = true
filter = nginx-limit-req
action = iptables-multiport
cloudflare
port = http,https
logpath = /var/log/nginx/*error*.log
findtime = 10
bantime = 6000
maxretry = 3
Ctrl+X, Y and Enter to Save
Test the CloudFlare fail2ban integration configuration
sudo fail2ban-client -d
You will see multiple actions are added for your fail2ban jail
['set', 'nginx-limit-req', 'addaction', 'iptables-multiport']
['set', 'nginx-limit-req', 'action', 'iptables-multiport', 'actionban', ' -I f2b- 1 -s -j ']
['set', 'nginx-limit-req', 'action', 'iptables-multiport', 'actionstop', ' -D -p -m multiport --dports -j f2b-\n -F f2b-\n -X f2b-']
['set', 'nginx-limit-req', 'action', 'iptables-multiport', 'actionstart', ' -N f2b-\n -A f2b- -j \n -I -p -m multiport --dports -j f2b-']
['set', 'nginx-limit-req', 'action', 'iptables-multiport', 'actionunban', ' -D f2b- -s -j ']
['set', 'nginx-limit-req', 'action', 'iptables-multiport', 'actioncheck', " -n -L | grep -q 'f2b-[ \\t]'"]
['set', 'nginx-limit-req', 'action', 'iptables-multiport', 'protocol', 'tcp']
['set', 'nginx-limit-req', 'action', 'iptables-multiport', 'chain', 'INPUT']
['set', 'nginx-limit-req', 'action', 'iptables-multiport', 'lockingopt', '-w']
['set', 'nginx-limit-req', 'action', 'iptables-multiport', 'known/known/name', 'default']
['set', 'nginx-limit-req', 'action', 'iptables-multiport', 'blocktype', 'REJECT --reject-with icmp-port-unreachable']
['set', 'nginx-limit-req', 'action', 'iptables-multiport', 'known/lockingopt', '-w']
['set', 'nginx-limit-req', 'action', 'iptables-multiport', 'known/known/port', 'ssh']
['set', 'nginx-limit-req', 'action', 'iptables-multiport', 'known/protocol', 'tcp']
['set', 'nginx-limit-req', 'action', 'iptables-multiport', 'known/known/lockingopt', '-w']
['set', 'nginx-limit-req', 'action', 'iptables-multiport', 'port', 'ssh']
['set', 'nginx-limit-req', 'action', 'iptables-multiport', 'known/known/chain', 'INPUT']
['set', 'nginx-limit-req', 'action', 'iptables-multiport', 'known/name', 'default']
['set', 'nginx-limit-req', 'action', 'iptables-multiport', 'known/known/protocol', 'tcp']
['set', 'nginx-limit-req', 'action', 'iptables-multiport', 'iptables', 'iptables ']
['set', 'nginx-limit-req', 'action', 'iptables-multiport', 'known/__name__', 'Init']
['set', 'nginx-limit-req', 'action', 'iptables-multiport', 'returntype', 'RETURN']
['set', 'nginx-limit-req', 'action', 'iptables-multiport', 'known/returntype', 'RETURN']
['set', 'nginx-limit-req', 'action', 'iptables-multiport', 'known/known/__name__', 'Init']
['set', 'nginx-limit-req', 'action', 'iptables-multiport', 'known/known/returntype', 'RETURN']
['set', 'nginx-limit-req', 'action', 'iptables-multiport', 'name', 'default']
['set', 'nginx-limit-req', 'action', 'iptables-multiport', 'known/known/blocktype', 'REJECT --reject-with icmp-port-unreachable']
['set', 'nginx-limit-req', 'action', 'iptables-multiport', 'known/port', 'ssh']
['set', 'nginx-limit-req', 'action', 'iptables-multiport', 'known/iptables', 'iptables ']
['set', 'nginx-limit-req', 'action', 'iptables-multiport', 'known/chain', 'INPUT']
['set', 'nginx-limit-req', 'action', 'iptables-multiport', 'known/blocktype', 'REJECT --reject-with icmp-port-unreachable']
['set', 'nginx-limit-req', 'action', 'iptables-multiport', 'known/known/iptables', 'iptables ']
['set', 'nginx-limit-req', 'addaction', 'cloudflare']
['set', 'nginx-limit-req', 'action', 'cloudflare', 'actionban', 'curl -X POST "https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules" \\\n-H "X-Auth-Email: " \\\n-H "X-Auth-Key: " \\\n-H "Content-Type: application/json" \\\n--data \'{"mode":"block","configuration":{"target":"ip","value":""},"notes":"Fail2ban"}\'']
['set', 'nginx-limit-req', 'action', 'cloudflare', 'actionstop', '']
['set', 'nginx-limit-req', 'action', 'cloudflare', 'actionstart', '']
['set', 'nginx-limit-req', 'action', 'cloudflare', 'actionunban', 'curl -s -X DELETE "https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules/$(']
['set', 'nginx-limit-req', 'action', 'cloudflare', 'actioncheck', '']
['set', 'nginx-limit-req', 'action', 'cloudflare', 'cfuser', 'CloudFlare-username']
['set', 'nginx-limit-req', 'action', 'cloudflare', 'cftoken', 'CloudFlare-API-Key']
['set', 'nginx-limit-req', 'action', 'cloudflare', 'known/cfuser', 'CloudFlare-username']
['set', 'nginx-limit-req', 'action', 'cloudflare', 'known/cftoken', 'CloudFlare-API-Key']
['start', 'nginx-limit-req']
You can restart fail2ban to activate the CloudFlare integration on your jail.
sudo service fail2ban restart
You can check your logs
sudo tail -f /var/log/fail2ban.log
Now when users violate a rule on your server they will be banned on both your server and CloudFlare, all automatically for convenience.
Sources
Create CloudFlare Firewall Rule
List CloudFlare Rules
Delete CloudFlare Firewall Rule
Setting Multiple Actions fail2ban
Integrate fail2ban with CloudFlare (old API)
DDoS Protection with CloudFlare and fail2ban (old API)
fail2ban Custom Action
Extracting JSON values with Bash
Question: this will ban the IPs for the domain only or ALL domains under the CloudFlare account? On CloudFlare Firewall, we can block an IP address for “this website” or “all websites”.
Good question LiewCF, if I recall correctly this does it for the singular domain because I am using the zoneid for the specific domain.
Good question LiewCF, if I recall correctly this does it for the singular domain by specifying the zoneid for that domain.
Hi How to specify Zone Id in the script? Thank you so much for this AWESOME guide.
Hello,
I am having this error on Fail2Ban log. The CURL works fine if I execute it as it is in jail configuration.
2017-02-16 01:31:39,030 fail2ban.actions [1167]: NOTICE [wordpress-xmlrpc] Ban 104.36.109.26
2017-02-16 01:31:39,148 fail2ban.action [1167]: ERROR curl -s -X POST “https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules”
-H “X-Auth-Email: REDACTED”
-H “X-Auth-Key: REDACTED”
-H “Content-Type: application/json”
–data ‘{“mode”:”block”,”configuration”:{“target”:”ip”,”value”:”104.36.109.26″},”notes”:”Fail2ban”}’ — stdout: ”
2017-02-16 01:31:39,148 fail2ban.action [1167]: ERROR curl -s -X POST “https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules”
-H “X-Auth-Email: REDACTED”
-H “X-Auth-Key: REDACTED”
-H “Content-Type: application/json”
–data ‘{“mode”:”block”,”configuration”:{“target”:”ip”,”value”:”104.36.109.26″},”notes”:”Fail2ban”}’ — stderr: ”
2017-02-16 01:31:39,148 fail2ban.action [1167]: ERROR curl -s -X POST “https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules”
-H “X-Auth-Email: REDACTED”
-H “X-Auth-Key: REDACTED”
-H “Content-Type: application/json”
–data ‘{“mode”:”block”,”configuration”:{“target”:”ip”,”value”:”104.36.109.26″},”notes”:”Fail2ban”}’ — returned 7
2017-02-16 01:31:39,149 fail2ban.actions [1167]: ERROR Failed to execute ban jail ‘wordpress-xmlrpc’ action ‘cloudflare’ info ‘CallingMap({‘ipjailmatches’: <function at 0x7f65065c2938>, ‘matches’: u’104.36.109.26 – – [16/Feb/2017:01:31:14 +0200] “POST /xmlrpc.php HTTP/1.0” 200 828 “-” “XML-RPC for PHP 3.0.1″n104.36.109.26 – – [16/Feb/2017:01:31:16 +0200] “POST /xmlrpc.php HTTP/1.0” 200 828 “-” “XML-RPC for PHP 3.0.1″n104.36.109.26 – – [16/Feb/2017:01:31:18 +0200] “POST /xmlrpc.php HTTP/1.0” 200 828 “-” “XML-RPC for PHP 3.0.1″n104.36.109.26 – – [16/Feb/2017:01:31:19 +0200] “POST /xmlrpc.php HTTP/1.0” 200 828 “-” “XML-RPC for PHP 3.0.1″n104.36.109.26 – – [16/Feb/2017:01:31:20 +0200] “POST /xmlrpc.php HTTP/1.0” 200 828 “-” “XML-RPC for PHP 3.0.1″n104.36.109.26 – – [16/Feb/2017:01:31:21 +0200] “POST /xmlrpc.php HTTP/1.0” 200 828 “-” “XML-RPC for PHP 3.0.1″n104.36.109.26 – – [16/Feb/2017:01:31:22 +0200] “POST /xmlrpc.php HTTP/1.0” 200 828 “-” “XML-RPC for PHP 3.0.1″n104.36.109.26 – – [16/Feb/2017:01:31:23 +0200] “POST /xmlrpc.php HTTP/1.0” 200 828 “-” “XML-RPC for PHP 3.0.1″n104.36.109.26 – – [16/Feb/2017:01:31:24 +0200] “POST /xmlrpc.php HTTP/1.0” 200 828 “-” “XML-RPC for PHP 3.0.1″n104.36.109.26 – – [16/Feb/2017:01:31:25 +0200] “POST /xmlrpc.php HTTP/1.0” 200 828 “-” “XML-RPC for PHP 3.0.1″n104.36.109.26 – – [16/Feb/2017:01:31:26 +0200] “POST /xmlrpc.php HTTP/1.0” 200 828 “-” “XML-RPC for PHP 3.0.1″n104.36.109.26 – – [16/Feb/2017:01:31:27 +0200] “POST /xmlrpc.php HTTP/1.0” 200 828 “-” “XML-RPC for PHP 3.0.1″n104.36.109.26 – – [16/Feb/2017:01:31:29 +0200] “POST /xmlrpc.php HTTP/1.0” 200 828 “-” “XML-RPC for PHP 3.0.1″n104.36.109.26 – – [16/Feb/2017:01:31:30 +0200] “POST /xmlrpc.php HTTP/1.0” 200 828 “-” “XML-RPC for PHP 3.0.1″n104.36.109.26 – – [16/Feb/2017:01:31:31 +0200] “POST /xmlrpc.php HTTP/1.0” 200 828 “-” “XML-RPC for PHP 3.0.1″n104.36.109.26 – – [16/Feb/2017:01:31:32 +0200] “POST /xmlrpc.php HTTP/1.0” 200 828 “-” “XML-RPC for PHP 3.0.1″n104.36.109.26 – – [16/Feb/2017:01:31:34 +0200] “POST /xmlrpc.php HTTP/1.0” 200 828 “-” “XML-RPC for PHP 3.0.1″n104.36.109.26 – – [16/Feb/2017:01:31:35 +0200] “POST /xmlrpc.php HTTP/1.0” 200 828 “-” “XML-RPC for PHP 3.0.1″n104.36.109.26 – – [16/Feb/2017:01:31:36 +0200] “POST /xmlrpc.php HTTP/1.0” 200 828 “-” “XML-RPC for PHP 3.0.1″n104.36.109.26 – – [16/Feb/2017:01:31:37 +0200] “POST /xmlrpc.php HTTP/1.0” 200 828 “-” “XML-RPC for PHP 3.0.1″‘, ‘ip’: ‘104.36.109.26’, ‘ipmatches’: <function at 0x7f65065c26e0>, ‘ipfailures’: <function at 0x7f65065c2758>, ‘time’: 1487201499.029979, ‘failures’: 20, ‘ipjailfailures’: <function at 0x7f65065c29b0>})’: Error banning 104.36.109.26
I checked my logs and cannot see any of these errors Joonas. I googled a bit and got nothing, try asking Cloudfare support about it. If you get any info I’d like to share it here if possible 🙂
This is fantastic, super simple and it worked first time. Thanks for creating the guide!
One minor enhancement that could be useful is adding the timestamp the ban was done so you can view it in the CloudFlare console. Is this possible? It’s a little beyond what I can do myself in fail2ban. I expect it’s as simple as adding the correct variable or similar in the action file, but I’m not quite sure how to do this myself.
Great idea Tim, under the [Init] section try adding
cftimestamp = $(date +%Y%m%d%H%M%S)
Then in this line for the unban action
–data ‘{“mode”:”block”,”configuration”:{“target”:”ip”,”value”:””},”notes”:”Fail2ban”}’
try making it –data ‘{“mode”:”block”,”configuration”:{“target”:”ip”,”value”:””},”notes”:”Fail2ban “}’
Please let me know how that goes 🙂
Unfortunately it failed. The first line setting the cftimestamp variable causes the error below. When I comment it out it works again.
2017-07-09 14:49:42,269 fail2ban.server : INFO Stopping all jails
2017-07-09 14:49:43,120 fail2ban.actions.action: ERROR iptables -D INPUT -p tcp -m multiport –dports http,https -j fail2ban-Wordpress
iptables -F fail2ban-Wordpress
iptables -X fail2ban-Wordpress returned 100
2017-07-09 14:49:44,284 fail2ban.jail : INFO Jail ‘wordpress-soft’ stopped
2017-07-09 14:49:46,133 fail2ban.jail : INFO Jail ‘wordpress-hard’ stopped
2017-07-09 14:49:46,133 fail2ban.server : INFO Exiting Fail2ban
Excellent walkthrough, works flawlessly!
My pleasure Neil 🙂
When i use “fail2ban-client -d” command i dont see any cloudflare entries even tho i have cloudflare.conf file in action.d directory
This usually happens if the Cloudflare jail itself is misconfigured Justas or the jail.d folder isn’t included in fail2ban’s main configuration file. It could also be a minor typo in the cloudflare jail itself
Hi I have an issue with unbanning, the Cloudflare API returns 400 Bad Request, will you be able to help?
https://serverfault.com/questions/910940/fail2ban-unban-action-fails-with-cloudflare
Hey Vini, my guess is this is a copy/paste error, if you try the individual commands in the guide do they work by copying and pasting?
They work individually but the nested command shows 400 Bad Request. It used to work sometime ago, now it doesn’t.
Seems like there could have been some typo then Vini this is still working for me. Maybe you can do a pastebin of what you have and do a diff to compare it with the raw text here in the tutorial?
Hi, someone posted a solution on serverfault, Cloudflare has changed the way JSON response is sent. Stripping first line makes it work again. You can check the URL above.
Thank you so much Vini, updating the guide with that info now!
Many Thanks for such article , but can you please share the Fail2Ban filter as well that will work with the error.log file of NginX ?
Thanks
Hey ary190! I have a few on here depending on what you want to do https://guides.wp-bullet.com/?s=nginx+fail2ban enjoy 🙂
Thanks Andrius, I love when services change their API logic 😉
When you copied from server fault to your website you missed out the and , so it didn’t work, I had to get it from Server Fault. Also, it might be good if you updated the relevant part of the original post to save people having to work their way down the page and updating work they just did. Nice article though, thanks for posting it.
When you copied from server fault to your website you missed out the and , so it didn’t work, I had to get it from Server Fault. It also misses the “notes” part after the IP block, which is handy to see in the CloudFlare console. Also, it might be good if you updated the relevant part of the original post to save people having to work their way down the page and updating work they just did. Nice article though, thanks for posting it.
Also, I suspect (but I’m not sure yet) that type substitutions don’t work in fail2ban 0.8.x, which unfortunately is the version in the Amazon Linux repository.
The delete was not working for me. I found that the nested get was creating the ID with a rogue space at the beginning. I could not see where that was coming from so I just added an extra transform to remove it.
.... | tr -d '"' | tr -d ' ' | head -n 1)"
Thank you for sharing :). I think I need to update this guide soon!
Great article and I found it easy to set up cloudflare/fail2ban with these instructions. I took it a bit further adding the date and jail name to Cloudflare’s notes. I give instructions on my blog https://yeogle.com/2020/02/09/fail2ban-cloudflare/ (You may just lift the examples if you don’t want my link)
Thank you for sharing, that will be helpful for users using Apache as well 🙂