# fail2ban behind Cloudflare

> Use fail2ban behind Cloudflare: restore the real visitor IP in nginx or Apache, ban at Cloudflare's edge with cloudflare-token and an API token, and handle Tunnel.
>
> Source: https://fail2ban.dev/guides/cloudflare/ · Updated 2026-10-25 · Checked against fail2ban 1.1.1
> fail2ban.dev is an independent guide, not the official fail2ban project.

To use fail2ban behind Cloudflare, first make your web server log the visitor's address from the `CF-Connecting-IP` header (nginx: `set_real_ip_from` for Cloudflare's ranges plus `real_ip_header CF-Connecting-IP`; Apache: `mod_remoteip`). Then ban at Cloudflare's edge with fail2ban's `cloudflare-token` action and a Cloudflare API token, because a local firewall ban only blocks Cloudflare's servers — the attacker keeps reaching your site through Cloudflare.

```ini
[Init]
cfzone  = <your zone ID>
cftoken = <your API token>
```
[nginx-http-auth]
enabled = true
backend = auto
action  = %(action_)s
          cloudflare-token
```

## Why Cloudflare breaks a normal fail2ban setup

With the orange cloud (proxied DNS) on, visitors connect to a Cloudflare data center, and Cloudflare opens a new connection to your server. Your server therefore sees:

- **Cloudflare's IP as the TCP peer.** nginx's `$remote_addr`, Apache's `%h` and every log line contain a Cloudflare address. fail2ban counts failures for that address and bans it — cutting off everyone who happens to be routed through that data center.
- **No direct packets from the attacker.** Even if fail2ban knows the attacker's real address, a firewall rule for it on your server never matches: the attacker's packets end at Cloudflare.

Cloudflare passes the real client in the `CF-Connecting-IP` header (and in `X-Forwarded-For`). It does not send `X-Real-IP`.

## Problem 1: log the real visitor IP

### nginx

Trust the header only from Cloudflare's published ranges. Generate the list from Cloudflare's own files instead of copying it from a blog post:

```bash
  for ip in $(curl -s https://www.cloudflare.com/ips-v4) $(curl -s https://www.cloudflare.com/ips-v6); do
    echo "set_real_ip_from $ip;"
  done
  echo "real_ip_header CF-Connecting-IP;"
} | sudo tee /etc/nginx/conf.d/cloudflare-realip.conf

sudo nginx -t && sudo systemctl reload nginx
```

Cloudflare changes the list occasionally; rerun the script from a monthly cron job. After the reload, the access log's first field and `client:` in the error log show visitors, and every stock nginx filter works as usual — see the [nginx guide](/guides/nginx/).

### Apache

```bash
  echo "RemoteIPHeader CF-Connecting-IP"
  for ip in $(curl -s https://www.cloudflare.com/ips-v4) $(curl -s https://www.cloudflare.com/ips-v6); do
    echo "RemoteIPTrustedProxy $ip"
  done
} | sudo tee /etc/apache2/conf-available/cloudflare-remoteip.conf

sudo a2enmod remoteip && sudo a2enconf cloudflare-remoteip && sudo systemctl reload apache2
```

### Verify

```bash
```

## Problem 2: ban at Cloudflare's edge

fail2ban ships two Cloudflare actions:

| Action | Auth | Creates rules at | Use |
|---|---|---|---|
| [`cloudflare-token`](/actions/#action-cloudflare-token) | API token (`Authorization: Bearer`) | One zone (`/zones/<cfzone>/firewall/access_rules/rules`) | Recommended |
| [`cloudflare`](/actions/#action-cloudflare) | Account e-mail + Global API Key | All zones of your user (`/user/firewall/access_rules/rules`) | Legacy; the Global API Key can do anything on your account |

Both create Cloudflare **IP Access Rules** with mode `block` and the note `Fail2Ban <jail>`, and delete the rule again when the ban expires. They need `curl` on the server.

### Create the API token

In the Cloudflare dashboard go to **My Profile → API Tokens → Create Token → Create Custom Token** and give it:

- **Permissions:** Zone → Firewall Services → Edit
- **Zone Resources:** Include → Specific zone → your domain

Copy the token, and copy the **Zone ID** from the domain's Overview page. The token can't do anything outside that zone's firewall settings, which is why the action's own comments recommend this permission alone.

### Configure the action

Keep secrets out of `jail.local` by putting them in a `.local` file for the action, readable only by root:

```ini
[Init]
cfzone  = 0123456789abcdef0123456789abcdef
cftoken = YOUR_API_TOKEN
```
sudo chmod 640 /etc/fail2ban/action.d/cloudflare-token.local
```

```ini
[nginx-http-auth]
enabled  = true
backend  = auto
maxretry = 5
bantime  = 1h
action   = %(action_)s
           cloudflare-token

[nginx-404]
enabled  = true
backend  = auto
filter   = nginx-4xx[codes="404"]
logpath  = /var/log/nginx/access.log
maxretry = 20
action   = cloudflare-token
```

- `cfmode` — `block` (default). The Cloudflare API also accepts `challenge`, `js_challenge` and `managed_challenge`, e.g. `cloudflare-token[cfmode=managed_challenge]`.
- `cftarget` — `ip` for IPv4 and `ip6` for IPv6; the action switches automatically.
- `notes` — the rule comment (`Fail2Ban <name>` by default), which the unban step uses to find the rule.

### Zone vs account rules

`cloudflare-token` writes zone-level rules, so a ban protects one domain. To ban across every zone in your account, override the API URL and use an account token (permission Account → Account Firewall Access Rules → Edit):

```ini
[Definition]
_cf_api_url = https://api.cloudflare.com/client/v4/accounts/<cfaccount>/firewall/access_rules/rules

[Init]
cfaccount = 0123456789abcdef0123456789abcdef
cftoken   = YOUR_ACCOUNT_API_TOKEN
```

### Test the edge ban

```bash
sudo fail2ban-client set nginx-http-auth banip 192.0.2.15
```

## Cloudflare Tunnel

With Cloudflare Tunnel, `cloudflared` opens an outbound connection to Cloudflare and forwards requests to your service locally. Every request reaches your web server from `cloudflared` — `127.0.0.1` or the cloudflared container's address — and there's no inbound port to firewall. Consequences:

- **Local bans can't work at all.** Banning `127.0.0.1` would break your site; banning the visitor's IP locally matches nothing. Use `action = cloudflare-token` only, without `%(action_)s`.
- **Real IP:** Cloudflare still adds `CF-Connecting-IP`. Trust it from where cloudflared connects: `set_real_ip_from 127.0.0.1;` (or the Docker network, e.g. `172.18.0.0/16`) with `real_ip_header CF-Connecting-IP;`.
- Keep `127.0.0.1/8` and `::1` in `ignoreip`, so a misconfigured real-IP setup never makes fail2ban ban the tunnel.

## Whitelist Cloudflare in ignoreip

If real-IP restoration ever fails, fail2ban starts banning Cloudflare edge addresses. As a safety net, put Cloudflare's ranges on the ignore list. fail2ban 1.1.1 can read them from a file:

```bash
```
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 file:/etc/fail2ban/cloudflare-ips.txt
```

Finally, consider firewalling your origin so it only accepts HTTP(S) from Cloudflare's ranges. Then attackers can't bypass Cloudflare by connecting to your server's IP directly, and the edge ban covers everything.

## FAQ

### Why does fail2ban not block attackers behind Cloudflare?

Because the attacker never connects to your server; Cloudflare does. A local firewall rule for the attacker's IP matches no packets, and a rule for the Cloudflare IP in your logs blocks Cloudflare instead. Restore the real IP with `CF-Connecting-IP` and ban at Cloudflare's edge with the `cloudflare-token` action.

### Which permissions does the Cloudflare API token for fail2ban need?

For the stock `cloudflare-token` action, create a custom token with Zone → Firewall Services → Edit, limited to the zone you protect. For account-wide rules via the `/accounts/` endpoint, the token needs Account → Account Firewall Access Rules → Edit. Store the token in `action.d/cloudflare-token.local` with mode 640.

### What is the difference between the cloudflare and cloudflare-token actions?

`cloudflare-token` authenticates with a scoped API token and creates zone-level IP Access Rules. The older `cloudflare` action uses your account e-mail and Global API Key, which has full access to your account, and creates user-level rules for all zones. Prefer `cloudflare-token`.

### Does fail2ban work with Cloudflare Tunnel?

Yes, but only with edge bans. All requests arrive from `cloudflared` on localhost, so local firewall bans are useless. Trust `CF-Connecting-IP` from `127.0.0.1` in your web server, keep localhost in `ignoreip`, and use `action = cloudflare-token` in the jails.

### Should I whitelist Cloudflare IPs in fail2ban?

As a safety net, yes: add Cloudflare's ranges to `ignoreip` (fail2ban 1.1.1 accepts `file:/path`). It prevents fail2ban from banning Cloudflare edge servers if real-IP restoration breaks. It doesn't replace the real-IP configuration, which is what lets fail2ban see and ban actual attackers.
