CrowdSec Behind Cloudflare: Who Actually Sees the Attacker's IP, Hands-On

When Cloudflare fronts your origin, CrowdSec only sees what your reverse proxy writes into the log: edge IPs unless trusted proxies are configured. A hands-on guide to CF-Connecting-IP, X-Forwarded-For spoofing, firewall-level versus app-level blocking, and the CrowdSec Cloudflare bouncer that moves the block to the edge.

CrowdSec Behind Cloudflare: Who Actually Sees the Attacker's IP, Hands-On

If you have been following the blocking conversation on this blog, you already know the first half: the Fail2ban vs CrowdSec post settled how both tools watch logs, write decisions, and hand them to bouncers, and the reverse proxy comparison showed where Caddy, Traefik, and Nginx Proxy Manager sit in front of everything. What neither post answered is the question that shows up the moment you put Cloudflare in front of your CrowdSec-protected origin: who actually sees the attacker's IP?

The short version up front: Cloudflare sees it, your origin does not, and everything downstream, CrowdSec included, only ever sees what your reverse proxy writes into the log. If you skip the trusted-proxy configuration, you end up banning Cloudflare's edge IPs instead of the attacker, or worse, you trust a spoofable header and let attackers poison your decisions. This post walks through trusted proxies, X-Forwarded-For spoofing, firewall-level versus app-level blocking, and the CrowdSec Cloudflare bouncer that moves the block to the edge.

The short version

  • Cloudflare terminates the TCP connection, so your origin sees edge IPs, not attackers. The real client address rides in headers, and headers are only trustworthy from peers you explicitly trust.
  • A firewall bouncer behind Cloudflare bans the wrong address. App-level bouncers see the real IP only after trusted proxies rewrite the log and remote_addr.
  • The CrowdSec Cloudflare bouncer pushes decisions into Cloudflare, either as IP lists plus firewall rules or as a Worker plus KV store, so the block happens before traffic reaches your origin.
  • Header spoofing is the attack that breaks everything. Trust X-Forwarded-For from the wrong place and attackers choose what CrowdSec bans.
Verified
  • CrowdSec Security Engine1.6.x current, 1.7 next
  • crowdsec-cloudflare-bouncer (classic)Current from CrowdSec apt repo / Docker Hub
  • crowdsec-cloudflare-worker-bouncerCurrent from CrowdSec apt repo; docs now recommend this one
  • Cloudflare IP rangesPublished at cloudflare.com/ips; ranges in this post checked 2026-08-28
  • nginx realip moduleBuilt into nginx
  • Traefik / CaddyTraefik v3.x forwardedHeaders; Caddy 2.x trusted_proxies / remote_ip

Checked 2026-08-28 against cloudflare.com/ips, developers.cloudflare.com restoring-original-visitor-ips, docs.crowdsec.net (Cloudflare bouncer, nginx bouncer, WAF reverse proxy how-to), and the CrowdSec apt repos. Both CrowdSec and Cloudflare move fast; re-check before you rely on exact versions.

What Cloudflare actually does to your logs

When a domain is proxied through Cloudflare, the attacker never connects to your server. They connect to a Cloudflare edge, and Cloudflare opens a second connection to your origin from one of its own IPs. Your access log, your firewall, and CrowdSec all see the edge IP, which is why the first symptom of a misconfigured setup is decisions against addresses like 173.245.x.x, which belong to Cloudflare, not to the attacker.

Cloudflare publishes its ranges at cloudflare.com/ips. The list is stable enough to paste into config, but it does change, so treat it like a dependency you refresh on a schedule, not a one-time setup. Before any trusted-proxy work, this is what the origin actually sees:

what the origin sees without trusted proxies
tail -n 3 /var/log/nginx/access.log
173.245.48.1 - - [28/Aug/2026:10:00:01 +0000] "POST /wp-login.php HTTP/2.0" 401 223 "-" "python-requests/2.31.0"
173.245.48.9 - - [28/Aug/2026:10:00:03 +0000] "GET /xmlrpc.php HTTP/2.0" 404 162 "-" "curl/8.5.0"

# the attacker's real IP is not in this log at all
# every line carries a Cloudflare edge address

The headers that carry the real IP

Cloudflare passes the original visitor address in HTTP headers. Two matter here. CF-Connecting-IP is set by Cloudflare to the client address and carries a single value, which is why Cloudflare's own docs recommend it when your server supports it. X-Forwarded-For is the standard forwarding header, and it can be a chain: the client appends, then every proxy appends. The last value is the closest hop to you.

Header

Who sets it

Safe to trust from

What it carries

CF-Connecting-IP

Cloudflare edge

Cloudflare ranges only

Single client address; recommended by Cloudflare

X-Forwarded-For

Client, then every proxy appends

Only your trusted proxy ranges

Chain of addresses; last hop is the closest proxy

remote_addr (TCP)

The connection itself

Always truthful, but it is the edge IP

What a firewall bouncer sees behind Cloudflare

The spoofing trap: X-Forwarded-For is not a fact

Here is the failure that gets people. X-Forwarded-For is a client-controlled header. Nothing stops an attacker from sending X-Forwarded-For: 198.51.100.77 on every request. If your reverse proxy trusts that header unconditionally, you have given every attacker a free ban button aimed at any IP they choose, plus a way to dodge bans by rotating fake addresses. This is exactly why trusted-proxy configuration exists: you only honor forwarded headers from peers you named.

Hands-on: teach your reverse proxy who to trust

nginx: the realip module

nginx solves this with the realip module. You list every Cloudflare range with set_real_ip_from, tell nginx which header to trust with real_ip_header, and enable recursive resolution so the last trusted proxy wins. The full IPv4 and IPv6 range set, checked against cloudflare.com/ips:

# /etc/nginx/conf.d/cloudflare-real-ip.conf
# keep this file in sync with https://www.cloudflare.com/ips/
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;
set_real_ip_from 2400:cb00::/32;
set_real_ip_from 2606:4700::/32;
set_real_ip_from 2803:f800::/32;
set_real_ip_from 2405:b500::/32;
set_real_ip_from 2405:8100::/32;
set_real_ip_from 2a06:98c0::/29;
set_real_ip_from 2c0f:f248::/32;

real_ip_header CF-Connecting-IP;
real_ip_recursive on;

Traefik: forwardedHeaders

Traefik v3 handles the same problem in the entrypoint configuration. The forwardedHeaders.trustedIPs list is the allowlist; everything outside it is ignored when Traefik reads forwarding headers. Same ranges, same discipline:

# traefik dynamic config, entrypoints section
entryPoints:
  websecure:
    address: ":443"
    forwardedHeaders:
      trustedIPs:
        - 173.245.48.0/20
        - 103.21.244.0/22
        - 104.16.0.0/13
        - 104.24.0.0/14
        - 172.64.0.0/13
        - 162.158.0.0/15
        - 198.41.128.0/17
        # ... every IPv4 and IPv6 range from cloudflare.com/ips

Caddy: trusted_proxies or the Cloudflare matcher pattern

Caddy 2.5 added a native trusted_proxies option on the reverse_proxy directive. Cloudflare's own docs show the stricter pattern: a remote_ip matcher restricted to Cloudflare ranges, which both rewrites the header and rejects direct non-Cloudflare connections:

example.com {
	@cloudflare {
		remote_ip 173.245.48.0/20 103.21.244.0/22 103.22.200.0/22 103.31.4.0/22 \
			141.101.64.0/18 108.162.192.0/18 190.93.240.0/20 188.114.96.0/20 \
			197.234.240.0/22 198.41.128.0/17 162.158.0.0/15 104.16.0.0/13 \
			104.24.0.0/14 172.64.0.0/13 131.0.72.0/22 \
			2400:cb00::/32 2606:4700::/32 2803:f800::/32 2405:b500::/32 \
			2405:8100::/32 2a06:98c0::/29 2c0f:f248::/32
	}
	handle @cloudflare {
		reverse_proxy 127.0.0.1:8080 {
			header_up X-Forwarded-For {http.request.header.CF-Connecting-IP}
		}
	}
	handle {
		respond "Access Denied" 403
	}
}

Make CrowdSec see the real IP

CrowdSec's nginx parser reads remote_addr from the access log line, and the nginx bouncer checks ngx.var.remote_addr on every request. Both inherit the realip rewrite: once nginx or your proxy has been taught the trusted ranges, the log and remote_addr contain the client address, and CrowdSec sees the attacker instead of the edge. There is nothing extra to configure on the CrowdSec side for the standard nginx setup, which is the point of doing the rewrite at the proxy.

verify CrowdSec is reading the real address
# check what the parser extracts from a real log line
sudo cscli explain --file /var/log/nginx/access.log --type nginx | head -n 20

# every alert and decision should carry the attacker's address,
# never a 173.245.x.x or 104.x.x.x edge IP
sudo cscli alerts list -a | grep -A2 source_ip
sudo cscli decisions list -i

Where to block: firewall, app, or edge

With the real IP in the logs, the next question is which bouncer should act on the decision. The answer follows directly from what each layer can see:

  • Firewall-level (crowdsec-firewall-bouncer). The netfilter or nftables bouncer sees the TCP source, which behind Cloudflare is an edge IP. Blocking there means banning Cloudflare ranges, which either fails to protect or takes down legitimate traffic sharing the same edge. It only makes sense for traffic that reaches you directly, such as SSH on a port you keep outside Cloudflare.
  • App-level (crowdsec-nginx-bouncer, Caddy bouncer). These run inside the web server and check remote_addr after the realip rewrite, so they see the attacker. They answer with a 403 or a captcha for banned IPs at the origin, which works fine behind Cloudflare once trusted proxies are configured.
  • Edge-level (Cloudflare bouncer). This reads decisions from CrowdSec's LAPI and pushes them into Cloudflare, where the block happens before traffic reaches your origin. It is the only option that keeps the attacker from consuming your bandwidth and your TLS terminator at all.

The mental model: the firewall bouncer protects the box, the app bouncer protects the response, and the Cloudflare bouncer protects the edge. Behind Cloudflare you want the edge and the app layers; the firewall layer is for the direct-access ports.

Hands-on: the Cloudflare bouncer

CrowdSec offers two Cloudflare bouncers. The classic crowdsec-cloudflare-bouncer syncs decisions into Cloudflare as IP lists plus firewall rules. The newer crowdsec-cloudflare-worker-bouncer deploys a Cloudflare Worker with a KV store, and the CrowdSec docs now recommend it for most setups. Both need a Cloudflare user API token with the exact permissions listed on the bouncer docs page, scoped to the accounts and zones you actually protect.

install and register the classic bouncer
sudo apt install crowdsec-cloudflare-bouncer

# register a bouncer key with the local LAPI
sudo cscli bouncers add cloudflare-bouncer
Api key for 'cloudflare-bouncer': 8f8f8f8f...

# dry run: create the Cloudflare lists and firewall rules, then exit
sudo crowdsec-cloudflare-bouncer -s
# /etc/crowdsec/bouncers/crowdsec-cloudflare-bouncer.yaml
crowdsec_lapi_url: http://127.0.0.1:8080/
crowdsec_lapi_key: <lapi-key>

cloudflare_config:
  accounts:
    - id: <cloudflare-account-id>
      token: <cloudflare-api-token>
      ip_list_prefix: crowdsec
      default_action: challenge
      zones:
        - zone_id: <zone-id>
          actions:
            - block
            - challenge
          update_frequency: 30s

crowdsec_update_frequency: 10s

The supported actions are managed_challenge, block, js_challenge, challenge. The bouncer also handles country and AS-scoped decisions, and the helper flag creates the lists and rules once so you can inspect them in the Cloudflare dashboard before the daemon takes over.

the worker bouncer, the docs' current recommendation
sudo apt install crowdsec-cloudflare-worker-bouncer

# generate the config from your Cloudflare account token and zone ids
sudo crowdsec-cloudflare-worker-bouncer -g <account-token>,<zone-ids> \
    -o /etc/crowdsec/bouncers/crowdsec-cloudflare-worker-bouncer.yaml

# review the config, set the LAPI key, then deploy the worker
sudo crowdsec-cloudflare-worker-bouncer -S -c \
    /etc/crowdsec/bouncers/crowdsec-cloudflare-worker-bouncer.yaml
Classic Cloudflare bouncer vs Worker bouncer
Featurecrowdsec-cloudflare-bouncer (classic)crowdsec-cloudflare-worker-bouncer
Where decisions landCloudflare IP lists + firewall rulesCloudflare KV store
Blocking actionblock, challenge, or js_challenge firewall ruleBan (custom HTML) or Turnstile captcha
Decision scopeIP, country, and AS decisionsIP-level decisions in KV
DeploymentDaemon on the origin hostCloudflare Worker deployed from the CLI or self-hosted installer
Docs statusAvailable, older approachRecommended by current CrowdSec docs
  • Where decisions land

    crowdsec-cloudflare-bouncer (classic)
    Cloudflare IP lists + firewall rules
    crowdsec-cloudflare-worker-bouncer
    Cloudflare KV store
  • Blocking action

    crowdsec-cloudflare-bouncer (classic)
    block, challenge, or js_challenge firewall rule
    crowdsec-cloudflare-worker-bouncer
    Ban (custom HTML) or Turnstile captcha
  • Decision scope

    crowdsec-cloudflare-bouncer (classic)
    IP, country, and AS decisions
    crowdsec-cloudflare-worker-bouncer
    IP-level decisions in KV
  • Deployment

    crowdsec-cloudflare-bouncer (classic)
    Daemon on the origin host
    crowdsec-cloudflare-worker-bouncer
    Cloudflare Worker deployed from the CLI or self-hosted installer
  • Docs status

    crowdsec-cloudflare-bouncer (classic)
    Available, older approach
    crowdsec-cloudflare-worker-bouncer
    Recommended by current CrowdSec docs

The limits that bite

  • Firewall bans behind Cloudflare are wrong by default. The netfilter bouncer sees edge IPs, so a decision it enforces either bans Cloudflare ranges or does nothing useful. Keep it for direct-access ports only.
  • A stale range list silently reverts the fix. When Cloudflare adds ranges and your set_real_ip_from or trustedIPs misses them, traffic from those ranges goes back to being attributed to edge IPs, and the whole chain breaks again without an error.
  • The Cloudflare bouncer only protects proxied zones. Grey-cloud (unproxied) DNS records are outside its reach, so those zones still need an app-level or firewall bouncer.
  • Header trust is binary. One wrong range in the trusted list makes the whole chain spoofable. Audit it as a security control, not a config detail.
  • The worker bouncer has a billing surface. Workers requests and KV storage are metered, and the docs warn about KV charges, including for zones that should have been excluded from scope.
  • CrowdSec still only sees what the log says. If an application behind the proxy writes its own logs with the edge IP, decisions derived from those sources inherit the same attribution problem.

Which should you pick?

  • Cloudflare in front of a web origin: use the Cloudflare bouncer for the edge block, and an nginx or Caddy bouncer behind it for the 403 or captcha response. Configure trusted proxies first, because both layers depend on the rewrite.
  • No proxy, or direct ports: the firewall bouncer is fine, because the TCP source is the attacker. Keep it scoped to the interfaces and ports that are actually exposed.
  • Cloudflare plus direct access on the same box: run the Cloudflare bouncer for the proxied zones and the firewall bouncer for the direct ports, and make sure the trusted ranges cover only the proxy.
  • You just want the logs to tell the truth: the trusted-proxy configuration alone fixes attribution. The bouncers decide where the block lands, but without the rewrite, none of them see the attacker.

Official sources

  • Cloudflare IP ranges: https://www.cloudflare.com/ips/
  • Restoring original visitor IPs (Cloudflare): https://developers.cloudflare.com/support/troubleshooting/restoring-visitor-ips/restoring-original-visitor-ips/
  • CrowdSec Cloudflare bouncer docs: https://docs.crowdsec.net/u/bouncers/cloudflare/
  • crowdsec-cloudflare-bouncer (GitHub): https://github.com/crowdsecurity/cs-cloudflare-bouncer
  • crowdsec-cloudflare-worker-bouncer (GitHub): https://github.com/crowdsecurity/cs-cloudflare-worker-bouncer
  • CrowdSec nginx bouncer: https://docs.crowdsec.net/u/bouncers/nginx/
  • CrowdSec WAF reverse proxy how-to: https://docs.crowdsec.net/u/user_guides/waf_rp_howto/
  • nginx realip module: https://nginx.org/en/docs/http/ngx_http_realip_module.html
  • Our Fail2ban vs CrowdSec post: https://systhoughts.com/posts/fail2ban-vs-crowdsec
  • Our reverse proxy comparison: https://systhoughts.com/posts/caddy-vs-traefik-vs-nginx-proxy-manager

Are you running CrowdSec behind Cloudflare? Which layer ends up doing the blocking for you, and did you ever catch a decision aimed at a Cloudflare edge instead of an attacker? Drop it in the comments.

Until next time, keep your systems thoughtful.

No comments yet