Restrict Origin Traffic to CDN Shield IPs with UFW and Nginx

by Fahim

A public IP on your origin server is an open door. The moment a scraper or bot finds your real server IP—whether through Censys, SSL certificate transparency logs, or old DNS records—they can hit your backend directly. When that happens, your edge WAF, rate limits, and caching layers are completely bypassed.

To lock down your server, you need to restrict ports 80 and 443 at both the firewall and web server layers. Here is how I configure Ubuntu’s UFW and Nginx so the server only responds to verified CDN Origin Shield IP ranges while keeping SSH and SSL renewals intact.

Terminal interface displaying UFW firewall and Nginx origin shield configuration
Terminal interface displaying UFW firewall and Nginx origin shield configuration

Why Edge Caching Fails Without Origin IP Restrictions

Slapping a CDN in front of your domain doesn’t hide your server. If someone sends an HTTP request straight to http://YOUR_SERVER_IP, Nginx will happily answer it unless you tell it not to.

Direct origin access creates three big headaches:

  • Cache stampedes: Scrapers hitting un-cached endpoints directly will hammer your CPU and database, blowing right past the rules we set up in our guide on configuring CDN origin shield and tiered caching.
  • Zero DDoS protection: Volumetric edge protection does nothing if traffic floods your VPS network card directly.
  • Header spoofing: Unfiltered origin traffic allows attackers to forge client headers like CF-Connecting-IP or custom authentication tokens.

A simple two-tier defense fixes this: drop unauthorized packets at the Linux kernel level with UFW, then validate edge headers inside Nginx.

Layer 1: Lock Down Ports with UFW

The cleanest way to drop bad traffic is at the network layer. If a packet doesn’t come from your CDN’s Origin Shield subnet or your own management IP, the kernel should discard it immediately without waking up Nginx.

First, make sure your SSH port is allowed so you don’t lock yourself out.

Run these commands to establish your base rules:

# 1. Reset UFW to default settings
sudo ufw default deny incoming
sudo ufw default allow outgoing # 2. Allow your administrative SSH access (replace 22 if using a custom port)
sudo ufw allow 22/tcp comment 'Admin SSH' # 3. Allow loopback traffic
sudo ufw allow in on lo to any

Next, whitelist the specific CIDR IP ranges published by your CDN provider’s Origin Shield or Edge tier. Here is an example adding sample subnets (like 198.51.100.0/24 and 203.0.113.0/24):

# Allow HTTP/HTTPS traffic ONLY from CDN Origin Shield ranges
sudo ufw allow proto tcp from 198.51.100.0/24 to any port 80,443 comment 'CDN Shield Range 1'
sudo ufw allow proto tcp from 203.0.113.0/24 to any port 80,443 comment 'CDN Shield Range 2' # Enable the firewall
sudo ufw enable # Verify active status and rules
sudo ufw status verbose

If you want to dig into custom rules, check out the Ubuntu UFW Documentation.

Layer 2: Nginx Real IP and Access Restrictions

UFW protects the interface, but Nginx still needs to know the client’s actual IP address behind the CDN shield. Without configuring the Nginx ngx_http_realip_module, every visitor in your access logs will show up with the CDN shield IP. That breaks analytics, rate limiting, and fail2ban.

Drop your trusted CDN proxy definitions into a snippet at /etc/nginx/conf.d/cdn-shield.conf:

# /etc/nginx/conf.d/cdn-shield.conf # Trust CDN Origin Shield IP ranges
set_real_ip_from 198.51.100.0/24;
set_real_ip_from 203.0.113.0/24; # Define the header containing the end-user client IP
real_ip_header X-Forwarded-For;
real_ip_recursive on;

Next, update your virtual host file (e.g., /etc/nginx/sites-available/app.conf) to enforce access control and verify a shared secret header sent from your edge:

server { listen 443 ssl http2; server_name isitdev.com; # Restrict access by IP at Nginx layer allow 198.51.100.0/24; allow 203.0.113.0/24; deny all; # Verify custom secret header added by CDN Origin Shield if ($http_x_origin_secret != "7f9a8d4e2b1c6a0f") { return 403; } location / { try_files $uri $uri/ /index.php?$args; }
}

This gives you two layers of verification: traffic must originate from an authorized IP block and provide the exact secret header your CDN injects.

Handling Let’s Encrypt ACME Challenges Behind a Firewall

Once you close port 80 to the public, automated SSL renewals will fail. Let’s Encrypt validates domains via HTTP-01 challenges, querying your server directly from random validation nodes around the globe. Because they don’t publish static IP ranges, UFW will drop their validation requests.

If you’re already hitting renewal timeouts, check our troubleshooting steps for fixing Certbot SSL auto-renewal failures on Nginx.

The cleanest fix is switching Certbot to DNS-01 verification. With DNS-01, Let’s Encrypt checks a TXT record on your DNS provider instead of hitting port 80, meaning your origin ports stay completely locked down.

Install the appropriate Certbot DNS plugin for your provider and run:

# Example generating a certificate using DNS-01 validation
sudo certbot certonly  --manual  --preferred-challenges dns  -d isitdev.com -d *.isitdev.com

For more details on challenge mechanics, read the official Let’s Encrypt Challenge Types Guide.

Automating Shield IP Updates with a Bash Script

CDNs expand their infrastructure and rotate IP subnets periodically. If you manage these firewall rules by hand, you will eventually end up with stale IPs and 502/504 errors on your site. Automate the updates with a quick cron script.

Save this script to /usr/local/bin/update-shield-ips.sh:

#!/bin/bash
set -euo pipefail TMP_FILE="/tmp/cdn_ips.txt"
NGINX_CONF="/etc/nginx/conf.d/cdn-shield.conf" # Download published IP ranges from CDN (sample endpoint)
curl -sSL https://api.cdnprovider.com/ips/origin-shields > "$TMP_FILE" if [ ! -s "$TMP_FILE" ]; then echo "Failed to fetch IP list. Aborting." exit 1
fi # Generate Nginx configuration snippet
echo "# Auto-generated CDN Shield IPs - do not edit manually" > "$NGINX_CONF"
while IFS= read -r ip; do [ -z "$ip" ] && continue echo "set_real_ip_from $ip;" >> "$NGINX_CONF" echo "allow $ip;" >> "$NGINX_CONF"
done > "$NGINX_CONF"
echo "real_ip_recursive on;" >> "$NGINX_CONF" # Test Nginx syntax and reload
nginx -t && systemctl reload nginx # Update UFW firewall rules
while IFS= read -r ip; do [ -z "$ip" ] && continue ufw allow proto tcp from "$ip" to any port 80,443 comment 'CDN Shield Auto'
done < "$TMP_FILE" rm -f "$TMP_FILE"
echo "CDN Origin Shield IP update completed successfully."

Make it executable and set it to run weekly in root’s crontab:

sudo chmod +x /usr/local/bin/update-shield-ips.sh
# Add to crontab
(crontab -l 2>/dev/null;
echo "0 3 * * 0 /usr/local/bin/update-shield-ips.sh > /var/log/cdn-ip-update.log 2>&1") | crontab -

Testing the Lock: Direct Origin vs CDN Edge

Once your rules are in place and Nginx is reloaded, run two quick checks to verify everything works.

1. Direct Origin Test: Try to hit your origin server IP directly from your local terminal:

# Replace with your actual server IP
curl -I --connect-timeout 5 http://198.51.100.50/

This request should either hang and timeout (dropped by UFW) or return a 403 Forbidden from Nginx.

2. CDN Shield Route Test: Now query your public domain through the CDN:

curl -I https://isitdev.com/

You should see an HTTP/2 200 OK along with your CDN’s cache status headers. If you have dynamic routes that need to bypass caching entirely, see our guide on bypassing CDN edge cache for authenticated routes.

Frequently Asked Questions

Will UFW block my SSH connection if I enable default deny?

Yes, if you enable the firewall before adding an SSH rule. Always run sudo ufw allow 22/tcp (or your custom SSH port) before running sudo ufw enable.

Can I use Cloudflare or Fastly alongside this UFW setup?

Yes. The process is identical. Fetch the published IP ranges from Cloudflare or Fastly and feed those CIDRs into your UFW script and Nginx’s set_real_ip_from directives.

Why does Nginx return 403 Forbidden after updating IP rules?

This happens when the CDN routes a request through a POP that isn’t included in your IP whitelist, or when the X-Origin-Secret header is missing or misspelled in your CDN delivery rules.

Does blocking direct IP traffic hurt SEO or crawlers?

No. Googlebot and other legitimate crawlers look up your public domain via DNS and connect through your CDN edge. They never need direct access to your origin IP.

Next Steps

With direct origin traffic locked down, make sure your caching layer handles backend refreshes efficiently by checking out our guide on configuring stale-while-revalidate and stale-if-error headers on Nginx.

all_in_one_marketing_tool