Many of us use CloudFlare to protect our websites and services from malicious activity. In this post, I’ll show you how to configure Fail2ban to block bad actors directly on CloudFlare.


Nginx setup

Before we start, make sure your Nginx logs contain real client IP addresses. If your logs still show CloudFlare IPs, follow my earlier guide: Handling Real IP in Nginx Behind Load Balancer and Cloudflare


CloudFlare setup

Even on the free plan, CloudFlare provides enough tools to make this integration work:

  • 5 WAF rules
  • 1 custom list (up to 10,000 records)

That’s more than enough for our purposes.

1. Create a Custom List

Follow the official guide: Create CloudFlare Custom Lists

  • Set Type to IP.
  • After creation, open your list and check the URL — it will look like this: https://dash.cloudflare.com/<account_id>/configurations/lists/<list_id>

Make a note of your <account_id> and <list_id> - you’ll need them later.

2. Create a WAF Rule

Next, create a custom WAF rule using this guide: CloudFlare Custom WAF Rules

Configuration:

  • Field → IP Source Address
  • Operator → is in list
  • Value → (select your custom list)
  • Action → Block
  • Order → First

This rule ensures any IP address added to your list will be instantly blocked on CloudFlare.

3. Create an API Token

You’ll need an API token for Fail2ban to communicate with CloudFlare. Go to: https://dash.cloudflare.com/profile/api-tokens

Then:

  • Click Create Token → Create Custom Token
  • Under Permissions, choose: Account → Account Filter Lists → Edit
  • Click Continue to Summary → Create Token
  • Save your API Token securely.

Fail2Ban

Now let’s integrate Fail2ban with CloudFlare. We’ll use the configuration script shared here . For your convenience, here’s the version without comments:

[Definition]
actionstart =
actionstop =
actioncheck =
actionban = curl -s -o /dev/null -X POST <_cf_api_prms> \
                 -d '[{"ip":"'"<cfip>"'","comment":"Created by fail2ban <name>"}]' \
                 <_cf_api_url>

actionunban = id=$(curl -s -X GET <_cf_api_prms> \
                   "<_cf_api_url>?search=<cfip>&per_page=1" \
                   | { jp --unquoted 'result[0].id | not_null(@, `""`)' 2>/dev/null; })
              if [ -z "$id" ]; then echo "<name>: id for <ip> cannot be found"; exit 0; fi;
              curl -s -o /dev/null -X DELETE <_cf_api_prms> \
                   -d '{"items":[{"id":"'"$id"'"}]}' \
                   <_cf_api_url>

_cf_api_url = https://api.cloudflare.com/client/v4/accounts/<cfaccountid>/rules/lists/<cfbanlistid>/items
_cf_api_prms = -H 'Authorization: bearer <cfapitoken>' -H 'Content-Type: application/json'

[Init]
cfapitoken = <API_TOKEN>
cfaccountid = <account_id>
cfbanlistid = <list_id>
cfip = <ip>

[Init?family=inet6]
cfip = $(fail2ban-python -c 'import sys; from fail2ban.server.ipdns import IPAddr; a = IPAddr(sys.argv[1]+"/"+sys.argv[2]); print(str(a))' "<ip>" 64)

Save this file as /etc/fail2ban/action.d/cloudflare_api.conf
Then, replace API_TOKEN, account_id, and list_id with your actual values.

Fail2Ban Jail Configuration

And finally in /etc/fail2ban/jail.local, add something like this:

[nginx-404]
enabled = true
bantime = 600
logpath = %(nginx_access_log)s
maxretry = 10
port = http,https
action = cloudflare_api

[nginx-botsearch]
enabled = true
bantime = 600
logpath = %(nginx_error_log)s
maxretry = 10
port = http,https
action = cloudflare_api

This configuration ensures that Fail2ban will detect repeated malicious requests and automatically block those IPs.


Conclusion

By combining Fail2ban with CloudFlare, you get the best of both worlds:

  • Fail2ban’s local log monitoring and pattern detection,
  • CloudFlare’s global edge protection to stop attacks before they reach your infrastructure.

This setup effectively offloads the blocking process to CloudFlare, reducing server load while keeping attackers out. It’s simple, efficient, and works even on the free plan.

If you already use CloudFlare and Nginx, integrating Fail2ban like this is a no-brainer and your security will thank you.