Skip to content

Instantly share code, notes, and snippets.

@Xunnamius
Last active May 1, 2024 17:10
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Xunnamius/6057a660d06bcf13cc1f478af9131423 to your computer and use it in GitHub Desktop.
Save Xunnamius/6057a660d06bcf13cc1f478af9131423 to your computer and use it in GitHub Desktop.
Fail2ban action.d for Cloudflare meant to replace the version that ships with fail2ban currently. This updated version of the action uses Cloudflare's 2023 v4 API (free tier WAF + free tier WAF lists) to ban hostile IPs.
# This version works with CF WAF (using zone rulesets) and obsoletes previous
# versions. This works will all CF account types. This action depends on curl
# and jp and will add/remove IPs from the $known_hostile_ips list. Creating the
# WAF rules need only be done once per zone. Creating the list need only be done
# once per account.
#
# Author: Bernard Dickens III (Xunnamius)
#
# Inspired by work from: Mike Rushton
# https://github.com/fail2ban/fail2ban/blob/master/config/action.d/cloudflare.conf
#
# 1. REQUIRES jp TO BE INSTALLED IN THE USER PATH! Install it here:
# https://github.com/jmespath/jp
#
# 2. ! IMPORTANT ! Set jail.local's permission to 640 because it contains one of
# your CF API tokens. Grab your account id, your api token, and your "hostile
# ip" list id before continuing (see end of file for details).
#
# 3. Create a new custom list. Name it known_hostile_ips.
# https://developers.cloudflare.com/waf/tools/lists
#
# 4. Ensure every zone you want fail2ban to protect has an enabled WAF ban rule
# referencing $known_hostile_ips.
# https://developers.cloudflare.com/waf/custom-rules
#
# 5. Use the fail2ban CLI and the Cloudflare dashboard/Traces to test and make
# sure everything is working properly. You may need to add a permission to
# your api token to ensure proper function. See the end of this file for
# details.
#
# To get your CloudFlare API Key:
# https://www.cloudflare.com/a/account/my-account
#
# CloudFlare API error codes: https://www.cloudflare.com/docs/host-api.html#s4.2
#
# Note that if you're using Nginx, Apache, Litespeed, etc, you need to modify
# your logs and/or your filters such that the real client IP is being captured
# and not Cloudflare's IPs.
[Definition]
# Option: actionstart
# Notes.: command executed on demand at the first ban (or at the start of
# Fail2Ban if actionstart_on_demand is set to false).
# Values: CMD
#
actionstart =
# Option: actionstop
# Notes.: command executed at the stop of jail (or 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> IP address
# <failures> number of failures
# <time> unix timestamp of the ban time
# Values: CMD
#
# API v4 WAF
actionban = curl -s -o /dev/null -X POST <_cf_api_prms> \
-d '[{"ip":"<ip>","comment":"Created by fail2ban <name>"}]' \
<_cf_api_url>
# Option: actionunban
# Notes.: command executed when unbanning an IP. Take care that the
# command is executed with Fail2Ban user rights.
# Tags: <ip> IP address
# <failures> number of failures
# <time> unix timestamp of the ban time
# Values: CMD
#
# API v4 WAF
actionunban = id=$(curl -s -X GET <_cf_api_prms> \
"<_cf_api_url>?search=<ip>&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]
# If you like to use this action with mailing whois lines, you could use the
# composite action action_cf_mwl predefined in jail.conf, just define in your
# jail:
#
# action = %(action_cf_mwl)
# # Your CF API Key
# cfapitoken =
# cfaccountid =
# cfbanlistid =
# Your Cloudflare User API Token. It will need "EDIT" level on the "Account
# Filter Lists" permission.
# https://dash.cloudflare.com/profile/api-tokens
cfapitoken =
# The identifier of the Cloudflare account used to update the hostile IP list.
cfaccountid =
# Your Cloudflare WAF "hostile ip" List id. You can find it using the API or or
# following the instructions here:
# https://community.cloudflare.com/t/what-token-permissions-for-ip-list-edits/525222/6
# https://api.cloudflare.com/client/v4/accounts/:cfaccountid/rules/lists
#
# Note that even free CF accounts get 1 free list with 10,000 slots, yay!
# https://dash.cloudflare.com/:cfaccountid/configurations/lists
cfbanlistid =
@Staene
Copy link

Staene commented Apr 26, 2024

Thanks for this! Considering the impending EOL of CF's Firewall Rules, this is particularly useful.

@Xunnamius
Copy link
Author

Glad it was useful!

@Staene
Copy link

Staene commented Apr 30, 2024

It was/is! Question. I've just noticed this — is there any reason that an IPv6 address would prevent the API trigger? IPv4 addresses are being blocked no problem, but IPv6 are not.

@Xunnamius
Copy link
Author

Xunnamius commented Apr 30, 2024

Ah, I did notice this problem a bit later. And I probably should have made a note in the script about it. You can see the cause of the issue here. Essentially, Cloudflare only accepts IPv6 addresses in CIDR notation with a /64 prefix instead of the more specific (technically /128 prefix) IPv6 addresses returned by fail2ban:

To specify an IPv6 address, enter it as a CIDR range with a /64 prefix, the largest supported prefix for IPv6 CIDR ranges.

For example, instead of 2001:db8:6a0b:1a01:d423:43b9:13c5:2e8f, enter one of the following:

2001:db8:6a0b:1a01:0000:0000:0000:0000/64
2001:db8:6a0b:1a01::/64 (using the double colon notation)

The IPv6 address topology describes the last 64 bits as the host identifier. Matching on a /128 prefix would identify a specific IPv6 address, but not the host in general. It would be possible for an attacker to change their specific IPv6 address from a single machine.

The version of this script I use in prod is tweaked to turn IPv6 addresses into their CIDR form before sending them through the Cloudflare API. Unfortunately I use my own custom CLI tool that does this for me, so sharing the tweaked version here wouldn't be very useful. Here's the couple lines from my CLI tool that turn IPv6 from fail2ban into a CIDR range that Cloudflare likes.

@robs4638
Copy link

robs4638 commented May 1, 2024

Really appreciate this, its a huge help as I'm preparing for the cloudflare change. I'm getting lost in step 4 though - I'm not clear on how to reference the known_hostile_ips. I read the doc and the migration guide mentioned there but I guess I'm missing something.

  1. Ensure every zone you want fail2ban to protect has an enabled WAF ban rule
    referencing $known_hostile_ips.
    https://developers.cloudflare.com/waf/custom-rules

@Staene
Copy link

Staene commented May 1, 2024

  1. Log into Cloudflare & go to your website.
  2. Go to Security > WAF > Custom Rules
  3. Create rule.
  4. Name it. Then select:
  • Field: IP Source Address
  • Operator: Is in list
  • Value: select list
  1. Do that for every website in your account that you want to be protected by this list. Keep in mind this script bans IPv4 addresses only. Because of an issue with how Cloudflare requires IPv6 addresses be formatted, they don't make it to the list.

@robs4638
Copy link

robs4638 commented May 1, 2024

Got it thank you. Really appreciate the quick response and work on this solution. I'm tracking your request around IPv6 too. fail2ban/fail2ban#3735. Not a dev so I'll yield to the experts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment