A website behind Cloudflare still needs a deliberate policy for direct connections to its origin server. I check who can reach that origin and which proxies are trusted to supply visitor addresses. Otherwise a request can bypass the intended edge route or present a misleading address.
I verify those boundaries at the server as well as through the public website. The useful result is a clear rule for accepted traffic, with an explicit route for any monitoring or maintenance access the setup needs.
Why the orange cloud is not enough
A proxied DNS record hides your server’s address from casual visitors. It does not hide it from people who look harder. Old DNS records, mail records on the same server and certificate transparency logs all give origin addresses away. Once someone has it, they can skip Cloudflare entirely: no firewall rules, no DDoS protection, no bot checks.
The fix is simple to describe. The server should only talk to Cloudflare, and to itself.
Gap one: trusting a header from anyone
Behind Cloudflare, every request comes from a Cloudflare address, so nginx is usually told to read the real visitor from the CF-Connecting-IP header. The trap is in who it trusts to send that header. It is common to see this in an nginx config:
set_real_ip_from 0.0.0.0/0;
real_ip_header CF-Connecting-IP;That says “believe this header from anyone on the internet”. Anyone hitting the origin directly can set CF-Connecting-IP: 1.2.3.4 and your logs, rate limits and bans will see 1.2.3.4. Trust it from Cloudflare’s published ranges only. I generate the list rather than paste it, so it stays current:
#!/usr/bin/env bash
# Rebuild the trusted Cloudflare ranges for nginx. Safe to run from cron.
set -euo pipefail
out=/etc/nginx/snippets/cloudflare-real-ip.conf
{
echo "# Generated $(date -u +%F) from https://www.cloudflare.com/ips/"
for range in $(curl -fsS https://www.cloudflare.com/ips-v4) $(curl -fsS https://www.cloudflare.com/ips-v6); do
echo "set_real_ip_from $range;"
done
echo "set_real_ip_from 127.0.0.1;"
echo "real_ip_header CF-Connecting-IP;"
} > "$out.new"
mv "$out.new" "$out"
nginx -t && systemctl reload nginxInclude that snippet in the http {} block in place of any catch-all. If anything sits between nginx and PHP, such as Varnish, have the front server overwrite the header on the way through, so a forged value can never ride along:
proxy_set_header CF-Connecting-IP $remote_addr;Gap two: answering traffic that skipped the edge
The obvious move is an allow list of Cloudflare ranges in each site. It looks right and blocks everyone, because by the time nginx checks allow and deny, the real IP module has already swapped the address for the visitor’s. Check the actual connection instead, which nginx keeps in $realip_remote_addr:
# http {} context: 1 when the TCP peer is Cloudflare or this machine
geo $realip_remote_addr $via_cloudflare {
default 0;
127.0.0.1 1;
::1 1;
173.245.48.0/20 1;
103.21.244.0/22 1;
# ...one line per Cloudflare range, generated like the list above
}Then, in the public server {} block of each fully proxied site:
set $origin_ok $via_cloudflare;
# Certificate renewals may still arrive directly.
if ($uri ~ "^/\.well-known/acme-challenge/") { set $origin_ok 1; }
if ($origin_ok = 0) { return 403; }If the same server also hosts sites that are not proxied, lock each proxied site individually rather than at the firewall. A blanket rule would cut the others off.
Test it from outside
# Through Cloudflare: expect 200
curl -s -o /dev/null -w '%{http_code}\n' https://example.com/
# Straight to the origin: expect 403
curl -s -o /dev/null -w '%{http_code}\n' --resolve example.com:443:203.0.113.10 https://example.com/
# A forged header should not reach your logs as the visitor
curl -s -o /dev/null -H 'CF-Connecting-IP: 1.2.3.4' https://example.com/Then check your access log and make sure 1.2.3.4 is nowhere to be seen.
While you are in the Cloudflare dashboard
- Set SSL to Full (strict) so Cloudflare checks your origin certificate.
- Raise the minimum TLS version to 1.2 and switch on HSTS once every hostname serves HTTPS.
- Turn on Bot Fight Mode and AI crawler blocking if you do not want scrapers training on your work.
- Keep mail records DNS-only. A proxied mail or DKIM record never verifies.
None of this is exotic. It is just the difference between a site that is behind Cloudflare and one that only looks like it is.