
After enabling a CDN, your origin access log may show only CDN node addresses. If rate limits use those addresses, multiple visitors can end up sharing the same allowance. Diagnose both the source of the TCP connection and the way proxies forward the original visitor address.
Nginx’s Real IP module can replace the client address using a specified request header, provided the connection comes from a configured trusted proxy.[1] This guide covers HTTP reverse proxies for operators managing CDN origins and Nginx. Replace the documentation addresses in the examples with your own configuration. The guide is based on official documentation; no production origin changes or verification tests were performed.
Map the path that reaches the origin
Common paths include visitor → CDN → Nginx and visitor → CDN → load balancer → Nginx. In the latter, Nginx’s immediate connection source may be an internal load balancer, while the CDN address appears in the forwarding chain.
Confirm what each layer does before configuring trust: which header it uses, whether it overwrites a visitor-supplied header, how it appends proxy addresses, and whether it preserves a dedicated visitor-IP header supplied by the CDN. Identical header names do not guarantee identical construction rules.
This guide uses X-Forwarded-For as an example. If your platform requires another header, follow its origin documentation and verify how intermediate proxies handle it. A field with a client-IP-like name is not sufficient evidence of trust. This guide does not assume that CloudFlew uses a particular header.
Check the module and log both addresses
Inspect the Nginx build arguments:
nginx -V 2>&1
The official documentation states that ngx_http_realip_module is not built by default and requires --with-http_realip_module.[1] If your binary lacks it, follow your distribution or image documentation to select a build that includes the module before applying these directives.
After Real IP processing, $remote_addr contains the replacement client address, while $realip_remote_addr retains the original connection address. The latter variable has been available since Nginx 1.9.7.[1] Recording both helps distinguish the visitor from the proxy connecting to the origin.
Define the format in the http block and place access_log in the target server block. Keep your existing log and add this temporary log to a test site.[2]
# http block log_format realip_check '$time_iso8601 client=$remote_addr ' 'peer=$realip_remote_addr ' 'xff="$http_x_forwarded_for" ' '"$request" $status'; # Target server block access_log /var/log/nginx/realip-check.log realip_check;
The xff field contains the original header and may include visitor-supplied content. Use it to inspect proxy behavior, not as an already verified identity. Apply your existing privacy and retention rules, and remove the temporary log when the investigation is complete.
Define specific trusted proxy ranges
The following snippet belongs in the target server block. 192.0.2.0/24 and 2001:db8::/32 are documentation ranges, not actual CDN networks.
# Placeholders: replace with the platform's current, verified origin proxy ranges set_real_ip_from 192.0.2.0/24; set_real_ip_from 2001:db8::/32; real_ip_header X-Forwarded-For; real_ip_recursive on;
set_real_ip_from defines trusted sources that correctly supply replacement addresses. real_ip_header selects the header to read. With recursion enabled and the original connection source trusted, Nginx uses the last non-trusted address in the header.[1]
Use the provider’s current official network list, covering the IPv4 and IPv6 addresses actually used for origin connections. Trusting 0.0.0.0/0 or ::/0 makes every connection source trusted; it is not a suitable remedy for an incomplete list.
If an internal load balancer connects directly to Nginx, include its precise address or necessary subnet and verify its header processing. Avoid trusting an entire private network for convenience: other services on that network may not be entitled to assert visitor identities.
Real IP configuration does not restrict access to the origin. Limit allowed connection sources according to your architecture. The internal load balancer must also restrict ingress or sanitize untrusted headers, or an external request could enter the trusted chain through it.
Inspect the complete chain for multiple proxies
Consider a visitor at 198.51.100.23, passing through a CDN node at the documentation address 192.0.2.10, then a load balancer at 10.20.0.10 that connects to Nginx:
TCP connection source: 10.20.0.10 X-Forwarded-For: 198.51.100.23, 192.0.2.10
If both the load balancer and CDN ranges are correctly trusted, recursive processing can skip the trusted CDN address at the end of the chain and select 198.51.100.23. If only the load balancer is trusted, the search stops at the untrusted CDN node, so the log still shows a node address. With recursion disabled, a trusted connection source is replaced by the final header address, which may also be the CDN node.[1]
These results depend on the upstream systems constructing the chain according to the rules you have verified. Taking the leftmost IP is not a universal solution: visitors can submit their own addresses, and the upstream overwrite or append behavior determines which content is reliable.
For a dedicated single-address visitor-IP header, verify how the platform generates it, whether intermediate layers preserve it, and whether direct requests could spoof it. The XFF chain example above does not establish the safety of a different header configuration.
Forward the verified address to the application
Once Nginx logs the expected address, check what the application reads. An application using only its connection source may see Nginx’s address. An application blindly trusting the original request header may reintroduce untrusted content.
If the backend supports X-Real-IP and accepts connections only from this Nginx proxy, explicitly set the header in the existing proxy location:
proxy_set_header X-Real-IP $remote_addr;
Restrict the backend’s trusted sources to the actual Nginx proxy and configure header handling according to the framework’s documentation.[4] Allowing arbitrary sources to assert client addresses defeats this boundary.
Nginx defines $proxy_add_x_forwarded_for as the incoming XFF value with $remote_addr appended. Without an incoming XFF header, it equals $remote_addr.[4] Appending after Real IP processing can duplicate the visitor address while retaining untrusted parts of the original header. Systems needing the complete proxy chain should agree on its construction rules. An internal service needing only one verified address may overwrite its output field, after checking whether other auditing or tracing systems depend on the original chain.
Nginx rate limits keyed by $binary_remote_addr depend on the client address; its official examples use that variable.[3] A Real IP change may move the allowance from each proxy node to each visitor address. Review the existing rate, burst settings, and monitoring separately, rather than combining the logging correction with broad rate-limit changes.
Validate a small scope before reloading production
Back up the affected configuration files and record the current header rules and trusted ranges. Start with a test site or one virtual host so that settings in the global http block do not unexpectedly affect other entry points.
Request a read-only test endpoint and compare the corresponding origin log entry. The request must reach the origin. A CDN cache hit may produce no origin log entry, which alone does not indicate a Real IP failure.
# Replace with a test hostname and a read-only path confirmed to reach the origin curl -sS -o /dev/null -D - https://test.example.com/realip-check # Simulate a client claiming an address; 198.51.100.77 is a documentation address curl -sS -o /dev/null -D - \ -H 'X-Forwarded-For: 198.51.100.77' \ https://test.example.com/realip-check
For an ordinary request, client should correspond to the test connection’s public egress address and peer to the actual origin proxy or internal load balancer. Account for NAT or a VPN. After adding the forged header, the selected address should still follow the verified platform and intermediate-proxy rules. An HTTP status code alone cannot establish that the test passed.
On an authorized test origin, --resolve can specify the connection address while keeping the hostname in the HTTPS URL.[5] Replace the origin placeholder below, and retain certificate verification rather than hiding a separate TLS problem.
curl -sS -o /dev/null -D - \ --resolve test.example.com:443:203.0.113.10 \ -H 'X-Forwarded-For: 198.51.100.77' \ https://test.example.com/realip-check
If the origin allows only proxy connections, its ingress rules should reject the direct request. If the test architecture permits direct access, a header from an untrusted source must not replace the connection address. Also test IPv6, the internal load balancer path, and requests without the selected header to confirm the expected behavior at each entry point.
Check configuration syntax before reloading through your deployment system. The following commands apply to a systemd-managed Nginx installation; containers and managed services need their own deployment procedures.
sudo nginx -t sudo systemctl reload nginx
If identities or rate limits become incorrect, restore the backed-up configuration affected by this change, check syntax again, reload, and repeat the verification. Restoring configuration cannot undo completed business operations, so begin with read-only requests.
After deployment, retain visibility into both visitor addresses and connection sources. Review CDN network changes and any load balancer changes to header handling. Consistent trust rules across application logs, Nginx logs, and rate limiting make subsequent failures easier to locate.
Official references
Sources verified on October 14, 2026. Network access to the Nginx documentation pages was unreliable; the corresponding files in Nginx’s official documentation repository were used for verification.
[1] Nginx: ngx_http_realip_module; official documentation source. The module, trusted sources, recursive processing, and original connection variables.
[2] Nginx: ngx_http_log_module; official documentation source. Log formats and configuration contexts.
[3] Nginx: ngx_http_limit_req_module; official documentation source. The official client-address-based rate-limit example.
[4] Nginx: ngx_http_proxy_module; official documentation source. Header settings and the XFF append variable.
[5] curl: official --resolve documentation source. Specifying connection addresses by hostname and port.