Direct origin traffic bypasses your edge cache, blows past your firewall, and burns server CPU on requests that should never hit your backend. The moment bots or scrapers discover your origin IP, they will pound it directly instead of pulling cached assets from your CDN edge.
In this guide, we’ll configure Nginx to drop every request that doesn’t come straight from your CDN Origin Shield IP addresses. We’ll build CIDR allowlists, handle the classic RealIP translation trap that accidentally locks out legitimate users, and wire up a script to keep those IP lists updated automatically.

Why Lock Down Your Origin to Shield IPs Instead of All Edge Nodes?
Most CDNs run hundreds or thousands of Points of Presence (PoPs) worldwide. If you allow every edge IP into your origin, your allowlist spans thousands of frequently shifting subnets. That wide surface area is a nightmare to maintain and keep secure.
When you enable an Origin Shield (like Fastly Shielding, Cloudflare Tiered Cache, or AWS CloudFront Origin Shield), edge PoPs fetch cache misses through a central shield node instead of going straight to you. Your origin only ever talks to a tiny, predictable set of IPs. We break down how this works under the hood in our guide on how to configure origin shield to prevent CDN origin overload.
Routing pulls through shield nodes also kills duplicate origin requests when multiple edge nodes miss the cache at the same time. If you run into origin thundering herds, check out how to prevent CDN cache stampedes with origin shield and request collapsing.
The RealIP Trap: Why Standard Allow Directives Break
Here is a painful gotcha I ran into on my first production rollout. If your Nginx config uses the ngx_http_realip_module to restore real client IPs from the X-Forwarded-For header, Nginx evaluates standard allow and deny directives against that rewritten client IP—not the connecting CDN shield IP.
When a visitor visits your site from home, Nginx swaps $remote_addr to their residential IP. If your server block has a deny all; after your CDN allowlist, Nginx throws an instant 403 Forbidden because that visitor’s home ISP isn’t in your shield subnet.
We solve this with two clean approaches:
- Use an Nginx
geomap on$realip_remote_addr(the actual TCP socket IP) instead of$remote_addr. - Enforce restrictions at the firewall layer (UFW/iptables) or evaluate access rules before the RealIP module overwrites the address.
Step 1: Create the Shield IP Include File
Keep your IP lists in a standalone snippet inside /etc/nginx/conf.d/ or /etc/nginx/snippets/. This keeps your virtual host blocks readable and lets automated scripts refresh IP ranges without touching site configs.
Create the snippet file on your origin:
sudo mkdir -p /etc/nginx/snippets
sudo nano /etc/nginx/snippets/cdn-shield-ips.confAdd the IPv4 and IPv6 CIDR blocks provided by your CDN provider’s shield nodes. For example:
# StackPath / CDN Origin Shield IP Ranges
# US East Shield
allow 151.139.128.0/19;
# US West Shield
allow 192.157.192.0/20;
# EU Shield
allow 185.229.226.0/24;
# IPv6 Shield Blocks
allow 2a02:26f0::/32;Save and close the file.
Step 2: Apply the Allowlist with RealIP Protection
Next, configure your virtual host to inspect the connecting TCP socket. We check $realip_remote_addr via an Nginx geo block so that rewritten visitor IPs won’t trigger false 403s.
Open your site config (e.g., /etc/nginx/sites-available/example.com.conf):
# Define the shield check in the http context or before the server block
geo $realip_remote_addr $is_valid_shield { default 0; # Include your trusted Shield IPs 151.139.128.0/19 1; 192.157.192.0/20 1; 185.229.226.0/24 1; 2a02:26f0::/32 1; # Allow local connections for health checks 127.0.0.1 1; ::1 1;
} server { listen 443 ssl http2; server_name origin.example.com; # Restore true visitor IP for application logging and rate limits set_real_ip_from 151.139.128.0/19; set_real_ip_from 192.157.192.0/20; set_real_ip_from 185.229.226.0/24; set_real_ip_from 2a02:26f0::/32; real_ip_header X-Forwarded-For; real_ip_recursive on; # Reject requests that bypassed the CDN shield if ($is_valid_shield = 0) { return 403 "Direct origin access forbidden."; } location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }
}With this setup, Nginx restores the real visitor IP for your application logs using X-Forwarded-For, while the geo block checks the actual TCP connection against $realip_remote_addr. Anyone trying to reach origin.example.com directly gets an immediate 403.
Step 3: Add a Custom Origin Secret Header
IP restrictions block raw internet scrapers, but on shared CDN platforms, another tenant could theoretically point their distribution at your origin hostname. Adding a shared secret header eliminates that risk entirely.
Set your CDN to send a custom header on every origin pull (like X-Origin-Verify: a918f8e2-c517-48f9-bcf3-40e10b1063df). Then enforce it inside your Nginx config:
# Map header inside the http block
map $http_x_origin_verify $origin_header_valid { default 0; "a918f8e2-c517-48f9-bcf3-40e10b1063df" 1;
} server { listen 443 ssl http2; server_name origin.example.com; # Require BOTH valid Shield IP and valid secret header if ($is_valid_shield = 0) { return 403; } if ($origin_header_valid = 0) { return 403; } location / { try_files $uri $uri/ /index.php?$args; }
}If you run into TLS issues after setting up custom origin tokens, check out our guide on how to fix 502 bad gateway and SSL handshake errors between CDN and origin.
Step 4: Automate Shield IP Updates with a Cron Script
CDNs rotate and add shield IPs over time. Updating them by hand is a recipe for an accidental outage. A simple Bash script can fetch the latest list, test the Nginx config, and reload safely.
Create /usr/local/bin/update-shield-ips.sh:
#!/usr/bin/env bash
set -euo pipefail GEO_FILE="/etc/nginx/snippets/shield-geo.conf"
TEMP_FILE=$(mktemp) # Example fetching from CDN public IP endpoint
# Replace with your CDN provider's IP API or static list URL
echo "# Auto-generated on $(date -u)" > "$TEMP_FILE" # Fetch and format IPs into Nginx geo syntax
curl -sS https://api.cdnprovider.com/ips/origin-shields | jq -r '.ips[]' | while read -r ip; do echo " $ip 1;" >> "$TEMP_FILE"
done # Check if file has content
if [ -s "$TEMP_FILE" ]; then mv "$TEMP_FILE" "$GEO_FILE" # Validate syntax before reloading Nginx if nginx -t > /dev/null 2>&1; then systemctl reload nginx echo "Shield IPs updated and Nginx reloaded successfully." else echo "Nginx configuration test failed. Reverting changes." >&2 exit 1 fi
else echo "Failed to fetch IP list. Leaving existing configuration intact." >&2 rm -f "$TEMP_FILE" exit 1
fiMake it executable and wire it up to cron:
sudo chmod +x /usr/local/bin/update-shield-ips.sh
sudo crontab -eAdd this line to run the update nightly at 3:15 AM:
15 3 * * * /usr/local/bin/update-shield-ips.sh >> /var/log/shield-update.log 2>&1Step 5: Test the Lock Down and Verify Responses
Never assume your allowlist works without testing both the blocked and allowed paths. Run these curl checks from your workstation.
First, hit your origin directly, bypassing the CDN:
curl -I https://origin.example.comYou should get a 403 Forbidden right away:
HTTP/2 403 server: nginx
date: Mon, 24 Feb 2025 14:10:12 GMT
content-type: text/plain
content-length: 32Now make a request through your public CDN hostname:
curl -I https://www.example.comThe request hits the edge, routes through your Origin Shield, passes the Nginx geo check, and returns a clean 200 OK.
Origin Shield Lockdown Checklist
- Get Shield CIDRs: Grab the dedicated shield ranges from your CDN rather than the massive global edge list.
- Fix RealIP evaluation: Match against
$realip_remote_addrin yourgeoblocks so client IP restoration doesn’t cause false 403s. - Enforce a secret header: Require
X-Origin-Verifyto block cross-tenant spoofing on shared CDNs. - Automate list syncs: Run a cron script with
nginx -tchecks before reloading. - Test both paths: Confirm direct origin requests fail with 403 while CDN proxy requests pass with 200.
Frequently Asked Questions
Why not use the native Nginx allow and deny directives?
The standard ngx_http_access_module (allow / deny) checks the post-RealIP value in $remote_addr. Once you set real_ip_header X-Forwarded-For;, Nginx swaps $remote_addr to the client’s home IP. Unless that visitor’s home IP is on your shield list, Nginx drops the connection. Using geo $realip_remote_addr checks the physical TCP connection before headers are rewritten.
What happens if a CDN adds a new shield IP before my cron job runs?
If traffic arrives from an unlisted IP before your script updates the allowlist, Nginx drops the connection with a 403, and the shield node returns a 502 or 504 to the user. Most CDN providers announce subnet changes well in advance via mailing lists or JSON feeds.
Can I restrict access at the UFW or iptables firewall level instead?
Yes. Dropping unauthorized packets at the firewall level saves CPU by skipping the TLS handshake entirely. The downside: updating firewall rules dynamically via script requires elevated root permissions, and firewall rules can’t inspect HTTP headers like X-Origin-Verify.
Do I still need SSL on my origin if I restrict IPs?
Yes, absolutely. IP restrictions block rogue traffic, but without HTTPS on the origin, traffic between your CDN shield and your server travels in plain text. Always run a valid TLS certificate on your origin.

