Bali, Indonesia
Insights

Blocking IPs From Your Access Log Won't Stop Bots Coming Through Cloudflare

October 26, 2026
0:00
When a site gets hammered by bots, the usual first move is to open the access log, find the IPs with the most requests, and block them in the firewall or .htaccess. Behind Cloudflare, that move rarely works. The bot traffic keeps coming, and some real visitors may start seeing Cloudflare error pages instead of the site. The IPs at the top of the log usually don't belong to the attacker. They belong to Cloudflare.
When a DNS record is proxied through Cloudflare (the orange cloud in the DNS panel), visitors never connect to your server directly. Their browser connects to a nearby Cloudflare edge server. That edge server then opens its own connection to your origin, the server where the site actually lives. From your web server's point of view, the TCP connection comes from Cloudflare. So the address Apache logs as %h, or Nginx logs as $remote_addr, is a Cloudflare edge IP. The same edge IP carries traffic for many different visitors, and one visitor's requests can arrive through several edge IPs over a short period. You can confirm this in a few minutes. Take a few of the top IPs from your log and run whois on them. If the result shows Cloudflare as the network owner, or the address falls inside one of the ranges Cloudflare publishes on its website, you're looking at the proxy, not the client.
BASH
# Top 10 source IPs in the access log (path depends on your setup)
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -n 10

# Check who owns one of them
whois 203.0.113.10 | grep -iE 'orgname|netname|org-name'
The IP in the example is a documentation address. Substitute one from your own output. Expect the whois field names to vary between regional registries, which is why the grep matches a few variants.
Blocking a Cloudflare edge IP at the origin firewall has two effects, and neither is what you want. First, the attack continues. The bot is talking to Cloudflare, not to your server, and Cloudflare will route its requests through whichever edge server is available. Block one edge IP and the next request arrives from another. From the log it looks like the attacker is rotating addresses. They aren't. You're watching Cloudflare's own servers take turns. Second, legitimate visitors get caught. Everyone else who happens to be routed through that edge server now can't reach the origin either. Cloudflare can't complete its connection, so it shows its own error page. A REJECT rule typically surfaces as a 521, and a silent DROP as a 522 timeout. These errors only hit visitors routed through the blocked edge IPs, so they look random and are hard to reproduce. That makes them easy to misdiagnose as a server or network problem. This is a common pattern on the support side. A customer sees a flood in the log, blocks a dozen IPs with CSF or a panel IP blocker, and a few hours later reports that the site is "down for some people." Usually the first check is whether any blocked address falls inside Cloudflare's ranges. There is a quieter version of the same problem. Tools like Fail2ban read the access log and ban whatever IP shows up in failed login attempts. Behind Cloudflare, they read the edge IP and ban that. The result is the same, just automated.
Cloudflare doesn't throw the visitor's address away. It passes it to your origin in HTTP request headers:
  • CF-Connecting-IP contains the single client IP as Cloudflare saw it.
  • X-Forwarded-For contains the client IP too, but it is a chain. Any proxy along the path can append to it, and the client can send its own value first.
For a site directly behind Cloudflare, CF-Connecting-IP is the simpler header to rely on because it holds one address. There is one rule that matters more than which header you pick. Only trust that header when the connection itself comes from Cloudflare. Anyone who knows your origin IP can connect to it directly and send CF-Connecting-IP: 1.2.3.4 with any value they like. If your server believes that header from every source, an attacker can make their requests look like they come from any address, including yours. Every correct setup below pairs the header with a list of trusted proxy ranges for exactly this reason.
Apache's mod_remoteip replaces the connection's client address with the value from a header, but only when the request arrives from a proxy you've listed as trusted. Once it's active, access logs, .htaccess rules like Require not ip, and PHP's REMOTE_ADDR all see the real visitor. In practice, the setup looks like this. Where the file lives and how the module is enabled depends on the distro and control panel. cPanel, Plesk, and plain Debian or RHEL-based installs each handle it differently.
APACHE
# Load mod_remoteip first (a2enmod remoteip on Debian/Ubuntu;
# other setups load it through their own module config)

RemoteIPHeader CF-Connecting-IP

# One line per range from Cloudflare's published IPv4 and IPv6 lists.
# Only requests from these ranges will have the header honored.
RemoteIPTrustedProxy 173.245.48.0/20
# RemoteIPTrustedProxy ...next range...
There's one step that's easy to miss. The default combined log format uses %h, and the module documentation recommends switching your LogFormat to %a so the log records the corrected client address. Without that change, the config can be working while the log still looks wrong, and that sends people in circles. After editing, run apachectl configtest (or apache2ctl configtest) before reloading. On LiteSpeed, the equivalent is a server-level setting that tells it to use the client IP from a header, with an option to only trust known proxies. The exact name and location depends on whether you run LiteSpeed Enterprise or OpenLiteSpeed and which version. Look in the server's general or security configuration rather than .htaccess.
Nginx does the same job with the realip module. Most distro packages include it, and nginx -V will show --with-http_realip_module if it's compiled in.
NGINX
# Inside the http block, or a file included from it
# One line per Cloudflare IPv4 and IPv6 range
set_real_ip_from 173.245.48.0/20;
# set_real_ip_from ...next range...;

real_ip_header CF-Connecting-IP;
With this in place, remote_addr becomes the visitor's IP for logging, allow/deny rules, rate limiting with limit_req, and whatever is passed upstream to PHP-FPM. The original edge address is still available as realip_remote_addr. That variable is worth adding to a custom log format while you're testing, so you can see both values side by side. Run nginx -t before reloading. After the reload, reproduce a request from a device you control, find it in the log, and confirm the IP matches the public IP of that device rather than a Cloudflare range. Cloudflare's ranges change from time to time. A list that was correct when you wrote the config can drift. If a new edge range isn't in your trusted list, requests from it will be logged with the edge IP again. Some admins fetch the published lists on a schedule and regenerate the include file. Others just review it periodically. Either works, as long as someone owns it.
Once logs show real client IPs, you can at least see who is attacking. Where you block them is a separate decision, and the origin firewall is still the wrong layer for proxied traffic. iptables, nftables, CSF, and firewalld all look at the packet's source address, and that is still the Cloudflare edge. Header rewriting happens inside the web server, after the firewall has already made its decision. That leaves two places where blocking actually works:
  • At Cloudflare. Rules there act on the real visitor before traffic reaches your server, and they apply across every edge location. Depending on the plan, this can be WAF custom rules, IP access rules, rate limiting, or a challenge for suspicious paths like wp-login.php or xmlrpc.php. If you use Fail2ban, look for an action that pushes bans to the Cloudflare API instead of the local firewall.
  • In the web server. Require not ip in Apache or deny in Nginx now sees the real IP once remoteip or realip is set up. The request still crosses Cloudflare and reaches your origin, so this doesn't save bandwidth or connection overhead. But it does stop the request before it reaches PHP, and that's often where the load comes from.
The firewall still has a job. Its job is to make sure that, on ports 80 and 443, only Cloudflare can reach the origin. If the origin IP leaked through an old DNS record, a mail header, or a subdomain that isn't proxied, a bot can skip Cloudflare entirely and hit the server directly. In that case, none of your Cloudflare rules apply. Restricting web ports to Cloudflare's ranges closes that path. It also makes the trusted-proxy list in Apache or Nginx safer, because nothing else can reach the server to send a fake header. Before you change firewall rules on a production server, make sure you have console or out-of-band access. Leave SSH and any panel ports untouched. A mistake in an allowlist can lock you out, and on some providers that's slow to undo. The practical takeaway is short. When an IP at the top of your log belongs to Cloudflare, don't block it. First fix the logging with mod_remoteip or real_ip so you can see who is really there, and trust the header only from Cloudflare's published ranges. Then block abusive visitors at Cloudflare or in the web server, and use the origin firewall only to make sure traffic can't bypass Cloudflare.