Nginx as Reverse Proxy for CDN Origin

· Priya Sharma · 9 min read

Advanced

When Nginx serves as a reverse proxy for CDN origin, its configuration directly impacts CDN cache efficiency, content freshness, and origin server protection. The proxy sits between the CDN edge network and your backend application, controlling what content gets cached, how clients are identified, and how traffic is distributed across backend servers. Optimizing this proxy layer is critical because every CDN cache miss flows through it — and a misconfigured origin proxy can negate the benefits of even the most sophisticated CDN setup.

This guide covers the complete Nginx reverse proxy configuration for CDN origin scenarios: upstream management, header manipulation for CDN integration, Cache-Control headers, real IP restoration, access restriction, and monitoring.

How Do You Configure the Basic Proxy Setup?

# Upstream block with multiple backends
upstream app_backend {
    least_conn;                           # Prefer server with fewest connections
    server 10.0.0.1:8080 max_fails=3 fail_timeout=30s;
    server 10.0.0.2:8080 max_fails=3 fail_timeout=30s;
    server 10.0.0.3:8080 backup;          # Only used when others fail
    keepalive 32;                          # Keep connections to backends open
}

server {
    listen 443 ssl http2;
    server_name origin.example.com;

    # SSL between CDN and origin
    ssl_certificate /etc/ssl/origin.pem;
    ssl_certificate_key /etc/ssl/origin.key;

    location / {
        proxy_pass http://app_backend;

        # HTTP/1.1 for upstream keepalive
        proxy_http_version 1.1;
        proxy_set_header Connection "";

        # Forward original request info
        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;

        # Timeouts
        proxy_connect_timeout 5s;
        proxy_read_timeout 60s;
        proxy_send_timeout 30s;

        # Buffer settings
        proxy_buffer_size 16k;
        proxy_buffers 4 32k;
        proxy_busy_buffers_size 64k;

        # Retry on failure
        proxy_next_upstream error timeout http_502 http_503;
        proxy_next_upstream_tries 2;
        proxy_next_upstream_timeout 10s;
    }
}

The keepalive 32 directive in the upstream block maintains persistent connections to backend servers, avoiding the overhead of establishing new TCP connections for each request. Combined with proxy_http_version 1.1 and an empty Connection header, this enables HTTP/1.1 keepalive to the backend.

How Do You Set Cache-Control Headers for CDN?

Proper Cache-Control headers from the origin are the primary mechanism for controlling CDN edge caching behavior:

Performance Comparison: Edge vs Origin Processing
Metric Edge Processing Origin Processing Improvement
Latency (p50)8–15 ms120–300 ms15–20x faster
Cache Hit Ratio85–95%N/AReduces origin load
TLS Handshake1–5 ms (nearby PoP)50–150 ms10–30x faster
Bandwidth Cost$0.01–0.04/GB$0.05–0.12/GB3–5x cheaper
Availability99.99% (multi-PoP)99.9% (single DC)10x fewer outages
# Static assets: long cache, immutable
location ~* \.(css|js|woff2|png|jpg|svg|ico)$ {
    proxy_pass http://app_backend;
    expires 365d;
    add_header Cache-Control "public, immutable" always;
}

# HTML pages: moderate CDN cache, short browser cache
location ~* \.html$ {
    proxy_pass http://app_backend;
    add_header Cache-Control "public, s-maxage=3600, max-age=300, stale-while-revalidate=86400" always;
    add_header CDN-Cache-Control "public, max-age=3600" always;
}

# API responses: CDN caches briefly, browser does not cache
location /api/ {
    proxy_pass http://app_backend;
    add_header Cache-Control "public, s-maxage=60, max-age=0, must-revalidate" always;
    add_header Vary "Accept-Encoding, Authorization" always;
}

# Private/authenticated content: no CDN caching
location /account/ {
    proxy_pass http://app_backend;
    add_header Cache-Control "private, no-store" always;
}

The s-maxage directive tells CDN edge servers how long to cache the content, independently from max-age which controls browser caching. This allows aggressive CDN caching (1 hour) with conservative browser caching (5 minutes), ensuring users get fresh content through stale-while-revalidate while reducing origin load.

How Do You Restore Real Client IPs?

When traffic passes through a CDN, Nginx sees the CDN edge server's IP — not the actual client. CDNs forward the real client IP in custom headers:

# Cloudflare IP ranges (update periodically)
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 131.0.72.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;

# Use Cloudflare's header
real_ip_header CF-Connecting-IP;

# For other CDNs, use X-Forwarded-For
# real_ip_header X-Forwarded-For;
# real_ip_recursive on;  # Handle multiple proxy layers

Once configured, Nginx's $remote_addr variable contains the real client IP. This affects access logs, rate limiting (limit_req_zone $binary_remote_addr), and any application logic that uses the client IP.

Nginx reverse proxy CDN origin architecture showing client request flow through CDN edge, origin proxy with real IP restoration, to backend application servers Nginx CDN Origin Architecture Client CDN Edge Nginx Origin Backend 1 Backend 2 Real IP restoration Cache-Control headers IP allowlist (CDN only) Upstream load balancing

How Do You Restrict Access to CDN IPs Only?

Restricting origin access to CDN IP ranges prevents attackers from bypassing CDN security features (WAF, DDoS protection, bot management) by accessing the origin directly:

server {
    # Allow CDN IP ranges only
    allow 103.21.244.0/22;    # Cloudflare
    allow 103.22.200.0/22;
    # ... (all CDN ranges)

    # Allow health check from monitoring
    allow 10.0.0.0/8;

    # Block everything else
    deny all;

    # Return custom 403 for blocked IPs
    error_page 403 @blocked;
    location @blocked {
        return 403 '{"error":"Direct access not allowed. Use CDN."}';
        add_header Content-Type application/json;
    }
}

How Do You Monitor the Proxy Layer?

Add diagnostic headers to track proxy behavior:

# Add cache and timing headers
add_header X-Upstream-Status $upstream_status always;
add_header X-Upstream-Response-Time $upstream_response_time always;
add_header X-Request-ID $request_id always;

# Log upstream response times
log_format cdn_origin '$remote_addr - $request_time $upstream_response_time '
                      '$upstream_status "$request" $status $body_bytes_sent '
                      '"$http_referer" "$http_cf_connecting_ip"';

access_log /var/log/nginx/cdn_origin.log cdn_origin;

Monitor $upstream_response_time to detect backend slowness before it causes 504 timeouts. Track $upstream_status to detect backend errors before they propagate. The $request_id helps correlate requests across CDN, proxy, and backend logs for debugging.

How Do You Handle WebSocket Proxying?

WebSocket connections require special proxy configuration because they upgrade from HTTP to the WebSocket protocol:

location /ws/ {
    proxy_pass http://app_backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_read_timeout 3600s;    # 1 hour timeout for idle WebSocket
    proxy_send_timeout 3600s;
}

The Upgrade and Connection headers are essential for the HTTP→WebSocket protocol upgrade. Without them, the proxy strips the upgrade headers and the WebSocket connection fails with a 502 error. The extended timeouts prevent idle WebSocket connections from being prematurely closed.

External References

References & Standards: HTTP/2 specification · RFC 7234