Cloudflare Proxy Broke My IP-Based Security. Here's How I Fixed It.
I've been using Cloudflare for years.
Like many infrastructure engineers, I knew exactly what happens when you enable the orange cloud: your reverse proxy no longer communicates directly with clients. Instead, every incoming connection appears to come from one of Cloudflare's edge servers.
Because of that, I deliberately kept Cloudflare Proxy disabled for several internal services. The trade-off was straightforward:
- Keep the real client IP and preserve IP-based security.
- Enable Cloudflare Proxy and gain Cloudflare's protection, while losing visibility of the actual client.
The limitation wasn't a lack of understanding. It was simply technical debt that I hadn't had time to solve properly.
Recently I decided to revisit the problem—not just to restore the original client IP, but to restore the entire security model built around it.
The Core Problem
Cloudflare changes the trust boundary of your infrastructure. At the network layer, your server no longer communicates with the client. It communicates with Cloudflare.
Before
Client
│
▼
HAProxy
│
▼
Backend
HAProxy trusts src
──────────────────────────────────────────────
After
Client
│
▼
Cloudflare
│
▼
HAProxy
│
▼
Backend
HAProxy must trust: CF-Connecting-IP
ONLY if the connection originated from Cloudflare.
That distinction affects every component that relies on the client's IP address. Without understanding this architectural change, it's easy to configure reverse proxies incorrectly—or assume security mechanisms are working when they are not.
What Changes When Cloudflare Proxy Is Enabled?
Without Cloudflare:
Client (203.0.113.42)
│
▼
HAProxy (sees: 203.0.113.42)
With Cloudflare Proxy enabled:
Client (203.0.113.42)
│
▼
Cloudflare
│
▼
HAProxy (now sees: 104.x.x.x)
HAProxy now sees 104.x.x.x, which belongs to Cloudflare. From HAProxy's perspective, every visitor has effectively become Cloudflare.
The Hidden Consequences
At first glance, everything still works. Websites load. Applications respond. TLS terminates correctly. But every mechanism that depends on the client IP quietly becomes inaccurate or breaks entirely.
IP Whitelisting
Rules like:
acl my_whitelist src -f /etc/haproxy/whitelist.lst
no longer compare against the client's address. They compare against Cloudflare. Your whitelist no longer protects what you expect it to protect.
Rate Limiting
Instead of counting requests per client, HAProxy starts counting requests per Cloudflare edge node. Multiple unrelated visitors may now share the same rate limit simply because they route through the same Cloudflare POP.
Stick Tables
Stick Tables become far less useful because every tracked address belongs to Cloudflare rather than the actual client.
Logging
Before Cloudflare:
{
"client": "203.0.113.42",
"status": 401
}
After Cloudflare:
{
"client": "104.x.x.x",
"status": 401
}
Troubleshooting becomes much harder because the real client disappears from the logs.
Restoring the Original Client IP
Fortunately, Cloudflare already sends the original client address in the CF-Connecting-IP header.
The important part is not restoring it. The important part is restoring it only for trusted Cloudflare connections:
acl from_cloudflare src -f /etc/haproxy/cloudflare.lst
http-request set-src hdr(CF-Connecting-IP) if from_cloudflare { hdr(CF-Connecting-IP) -m found }
Once src is restored, every existing rule that already relies on `src` immediately becomes useful again. Without changing your ACLs, you restore:
- IP Whitelisting
- Rate Limiting
- Stick Tables
- Logging
- GeoIP filtering
Sometimes a single line of configuration restores an entire security model.
Why Fail2ban Is Different (The Catch-22)
At this point it's tempting to assume that Fail2ban will also start working. It won't.
This is where understanding the network stack becomes important. HAProxy restores the client IP at the HTTP layer (L7). Fail2ban ultimately blocks traffic using the host firewall (L3/L4). The firewall operates on network packets, not HTTP headers.
Every TCP connection arriving at your server still originates from Cloudflare. The original client IP never appears in the packet headers. So even if Fail2ban correctly extracts 203.0.113.42 from your HAProxy logs, it eventually executes something like:
iptables -A INPUT -s 203.0.113.42 -j DROP
+--------------------------+
| Fail2ban | (reads logs)
+--------------------------+
│
▼
iptables -A INPUT -s 203.0.113.42 -j DROP
│
▼
Linux Firewall
▲
│
Incoming TCP packet
src = 104.x.x.x (Cloudflare)
That rule never matches any incoming packets because the kernel only sees Cloudflare's source address. The attacker continues reaching your server through Cloudflare. The ban exists—but it has no effect.
Making Fail2ban Work
There are two practical approaches:
- Block at the edge: Instead of writing local firewall rules, configure Fail2ban to call the Cloudflare API and block the offending address at Cloudflare itself. The malicious traffic never reaches your infrastructure.
- Block inside HAProxy: Since HAProxy already restored the client identity, it can enforce application-level blocking using stick tables, maps, or dynamically updated ACLs. In this architecture, HAProxy—not the host firewall—becomes the correct enforcement point.
But Don't Trust the Header Blindly
This is the most important part. Anyone can send an HTTP header called `CF-Connecting-IP`.
If your HAProxy is directly accessible from the Internet and you blindly trust that header, an attacker can spoof any source address. The correct approach is to trust `CF-Connecting-IP` only when the request originates from Cloudflare's published IP ranges.
Even better, restrict your system firewall so that only Cloudflare IPs can connect to HAProxy on ports 80/443 in the first place.
Useful Links
- Official Cloudflare IP Ranges — The official list of IPv4 and IPv6 ranges to populate your trusted proxy list.
Final Thoughts
Enabling Cloudflare Proxy changes your infrastructure's trust boundary. It is not just a logging cosmetic change—it is an architectural shift. From now on, every security tool (from ACLs and rate limits to SIEMs and audit logs) must rely on verified identity.
The case with Fail2ban illustrates how simply restoring headers in your proxy does not solve the issue at lower layers of the network stack. Every security mechanism must be evaluated based on how it identifies the client: at the HTTP layer or at the network packet layer.
Restoring the original client IP inside HAProxy is not just a configuration trick for cleaner reports. It is about restoring a trusted source of truth that every subsequent security decision depends on.