#4187·fail2ban

[BR]: Cloudflare action: actionunban fails to extract rule ID when jail name has spaces

Author: whankleeCreated May 31, 2026Updated Jun 26, 2026
Labelsbug

Environment:

  • Fail2Ban version: 1.0.2
  • OS, including release name/version: Ubuntu 24.04.4 LTS (codename: noble)
  • Fail2Ban installed via OS/distribution mechanisms
  • You have not applied any additional foreign patches to the codebase
  • Some customizations were done to the configuration (provide details below is so)

The issue:

The actionunban command in action.d/cloudflare.conf silently fails to unban an IP address if the jail's name parameter contains a space (e.g., name="My Jail Name").

This happens because the lookup query URL includes &notes=Fail2Ban%%20<name>. When <name> contains a space, it results in an unencoded space in the curl URL (or invalid API query parameter), causing the Cloudflare API to return an unexpected response (or empty result), which makes the ID extraction fail.

Furthermore, the script contains a hidden error suppression: if [ -z "$id" ]; then echo "<name>: id for <ip> cannot be found"; exit 0; fi; Because it exits with exit 0, Fail2Ban assumes the action completed successfully, logging a false positive NOTICE ... Unban <ip>, while the IP remains permanently blocked on Cloudflare's firewall.

Steps to reproduce

  1. Configure a jail to use the Cloudflare action with a name containing a space, for example:
[wordpress-login]
enabled  = true
filter   = wordpress-login
logpath  = /var/log/apache2/*access*.log
findtime = 1h
bantime  = 24h
maxretry = 5
action   = cloudflare[name="<F-USER> wp-login"]

[wordpress-xmlrpc]
enabled  = true
filter   = wordpress-xmlrpc
logpath  = /var/log/apache2/*access*.log
findtime = 1h
bantime  = 48h
maxretry = 5
action   = cloudflare[name="<F-USER>: xmlrpc"]
  1. Ban a test IP. (The ban succeeds because actionban does not filter by note via GET). For testing purposes, I manually executed the following command to verify the behavior: sudo fail2ban-client set wordpress-login banip 1.2.3.4

  2. Unban the IP manually or wait for the bantime to expire. sudo fail2ban-client set wordpress-login unbanip 1.2.3.4

Expected behavior

  1. The IP should be successfully unbanned from Cloudflare.
  2. If the API lookup fails to find the rule ID, the action script should exit with a non-zero status (exit 1) so that Fail2Ban logs an ERROR instead of a successful NOTICE.
  3. (Optional but recommended) The &notes=Fail2Ban%%20<name> filter should be removed from the GET query in actionunban altogether, as Cloudflare's v4 API does not officially support filtering GET access rules by the notes field anyway, and the IP (configuration_value=<ip>) is already a unique identifier.

Observed behavior

The IP is removed from Fail2Ban's internal database and the log shows a successful unban message, but the IP is never released from Cloudflare. No errors are shown in fail2ban.log.

Any additional information

Removing &notes=Fail2Ban%%20<name> from the lookup URL and changing exit 0 to exit 1 completely resolves the issue.

Configuration, dump and another helpful excerpts

Any customizations done to /etc/fail2ban/ configuration

In jail.local:

ini
[wordpress-login]
enabled  = true
filter   = wordpress-login
logpath  = /var/log/apache2/*access*.log
action   = cloudflare[name="<F-USER> wp-login"]

The original actionunban configuration in action.d/cloudflare.conf:

ini
actionunban = id=$(curl -s -X GET <_cf_api_prms> \
                   "<_cf_api_url>?mode=block&configuration_target=<cftarget>&configuration_value=<ip>&page=1&per_page=1&notes=Fail2Ban%%20<name>" \
                   | { jq -r '.result[0].id' 2>/dev/null || tr -d '\n' | sed -nE 's/^.*"result"\s*:\s*\[\s*\{\s*"id"\s*:\s*"([^"]+)".*$/\1/p'; })
              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> "<_cf_api_url>/$id"

After editing and fixed configuration in action.d/cloudflare.conf:

ini
actionunban = id=$(curl -s -X GET <_cf_api_prms> \
                   "<_cf_api_url>?mode=block&configuration_target=<cftarget>&configuration_value=<ip>&page=1&per_page=1" \
                   | { jq -r '.result[0].id' 2>/dev/null || tr -d '\n' | sed -nE 's/^.*"result"\s*:\s*\[\s*\{\s*"id"\s*:\s*"([^"]+)".*$/\1/p'; })
              if [ -z "$id" ]; then echo "<name>: id for <ip> cannot be found"; exit 1; fi;
              curl -s -o /dev/null -X DELETE <_cf_api_prms> "<_cf_api_url>/$id"

UPD. Here it is as diff for better illustration what was changed:

diff
 actionunban = id=$(curl -s -X GET <_cf_api_prms> \
-                   "<_cf_api_url>?mode=block&configuration_target=<cftarget>&configuration_value=<ip>&page=1&per_page=1&notes=Fail2Ban%%20<name>" \
+                   "<_cf_api_url>?mode=block&configuration_target=<cftarget>&configuration_value=<ip>&page=1&per_page=1" \
                    | { jq -r '.result[0].id' 2>/dev/null || tr -d '\n' | sed -nE 's/^.*"result"\s*:\s*\[\s*\{\s*"id"\s*:\s*"([^"]+)".*$/\1/p'; })
-              if [ -z "$id" ]; then echo "<name>: id for <ip> cannot be found"; exit 0; fi;
+              if [ -z "$id" ]; then echo "<name>: id for <ip> cannot be found"; exit 1; fi;
               curl -s -o /dev/null -X DELETE <_cf_api_prms> "<_cf_api_url>/$id"

Relevant parts of /var/log/fail2ban.log file:

Even though the Cloudflare unban failed under the hood, Fail2Ban logs it as a successful notice because of the exit 0:

2026-05-31 09:07:33,274 fail2ban.actions         [453717]: NOTICE  [wordpress-login] Ban 1.2.3.4
2026-05-31 09:08:42,776 fail2ban.actions         [453717]: NOTICE  [wordpress-login] Unban 1.2.3.4

Relevant lines from monitored log files:

Not applicable (this is an action/API integration bug, not a filter issue).