How to Integrate fail2ban with CloudFlare API v4 Guide

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

32 thoughts on “How to Integrate fail2ban with CloudFlare API v4 Guide”

  1. 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.

  2. 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 🙂

  3. 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

  4. 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

    • 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.

  5. 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

    • 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.

  6. 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)"

Comments are closed.