Lock Your Origin: Cloudflare Doesn't Protect a Server Anyone Can Reach Directly
A CDN or WAF only filters traffic that passes through it. If your server answers on its own IP, an attacker can skip the edge entirely. How to check, how to lock nginx to the edge correctly, and the traps that break sites.
When the October 2026 PHP vulnerabilities were announced, many teams relied on their CDN's emergency WAF rules to cover the gap until packages arrived. Those rules only see traffic that passes through the CDN. If your origin server answers requests sent straight to its IP address, an attacker can bypass the edge and its rules entirely.
Check whether your origin is reachable
Send a request for your own site directly to the origin IP, skipping DNS:
curl -s -o /dev/null -w '%{http_code}\n' --resolve example.com:443:203.0.113.10 https://example.com/
If that returns 200, anyone who learns the IP — from old DNS records, mail headers or a scan — can talk to PHP directly.
Lock nginx to the edge, correctly
Most setups behind Cloudflare already restore the visitor's address with nginx's realip module:
set_real_ip_from 173.245.48.0/20; # …and the rest of Cloudflare's ranges
real_ip_header CF-Connecting-IP; # or X-Forwarded-For
This creates the first trap. After realip runs, $remote_addr is the visitor's address, not the address that connected to you. A filter on $remote_addr therefore either blocks everybody or nobody. Filter on $realip_remote_addr, which keeps the original connecting address:
# http {} level, e.g. /etc/nginx/conf.d/cf-only.conf
geo $realip_remote_addr $from_edge {
default 0;
127.0.0.1 1;
::1 1;
203.0.113.10 1; # the server's own address(es)
173.245.48.0/20 1; # Cloudflare IPv4 and IPv6 ranges, one per line
# …
}
# inside each server {} that is fully proxied
if ($from_edge = 0) { return 444; }
444 makes nginx close the connection without a response. Keep the range list current from Cloudflare's published IP lists.
The traps that break sites
- Server-internal clients. If an application reaches another site on the same server through
/etc/hosts(for example pointing a hostname at127.0.0.1), the loopback and the server's own addresses must stay allowed. - Not every hostname is proxied. Apply the rule only to server blocks whose hostnames all resolve to the edge. Wildcard subdomains, customer domains attached by CNAME to a SaaS application, and "grey-cloud" (DNS-only) records reach the origin directly by design; locking those vhosts would take them offline.
- Redirects that run first. Directives in server context run in order. A
rewrite … redirectplaced before your include still answers direct requests. That is usually harmless, because it never reaches PHP, but test real paths too, not just/. - Stale DNS after you turn the proxy on. When you switch a record from DNS-only to proxied, resolvers keep the old origin IP until the record's TTL expires. For a few minutes some visitors still connect directly, and the lock drops them. Enable the proxy first, wait out the TTL, then lock.
- TLS mode. Before proxying a site, make sure the origin has a valid certificate for every hostname and that the edge connects with full (strict) validation, or you trade one problem for another.
Stronger options
- Firewall-level allowlist: allow ports 80/443 only from the edge's ranges, so the web server never sees other traffic. Watch for anything else that legitimately connects to 80/443.
- Authenticated Origin Pulls: the edge presents a client certificate and the origin requires it. A forged request from elsewhere fails even if the IP list is wrong.
- Tunnels: the origin makes an outbound connection to the edge and exposes no public web port at all.
Verify, then keep verifying
After the change, run the curl --resolve test again for every locked site: it should fail to connect, and the normal URL should still return 200. Add both checks to your monitoring. New sites added to the server later are not covered automatically, so make the lock part of your site-creation checklist.
Sources
- Cloudflare IP ranges — Cloudflare
- Authenticated Origin Pulls — Cloudflare
- nginx ngx_http_realip_module ($realip_remote_addr) — nginx
- nginx ngx_http_geo_module — nginx