Restoring real client IPs in Nginx becomes necessary when Cloudflare, a load balancer, a CDN, or an internal reverse proxy connects to the origin first. Without a trusted RealIP configuration, Nginx records the proxy address in $remote_addr, which makes logs, rate limits, deny rules, and application audit trails point at the proxy instead of the visitor.
This is a trust-boundary task, not only a logging tweak. The RealIP module can replace $remote_addr with a header value, but it should do that only for requests that arrive from Cloudflare IP ranges or from proxy addresses you control.
Choose the Trusted Client IP Source
The Nginx RealIP module changes the client address used by Nginx variables after a request arrives from a trusted proxy. Once configured, $remote_addr becomes the restored client IP, while $realip_remote_addr keeps the original direct peer address, usually the proxy that connected to Nginx.
The official Nginx RealIP module documentation defines the directives used in this workflow:
| Directive or Variable | Context | Purpose |
|---|---|---|
set_real_ip_from | http, server, location | Marks a proxy IP address or CIDR range as trusted for real-IP replacement. |
real_ip_header | http, server, location | Names the request header or PROXY protocol source that carries the original client address. |
real_ip_recursive | http, server, location | Chooses the last non-trusted address from a forwarded chain when multiple trusted proxies are involved. |
$realip_remote_addr | Nginx variable | Stores the original direct peer address after RealIP processing. |
Check whether your Nginx build includes the HTTP RealIP module:
nginx -V 2>&1 | tr ' ' '\n' | grep -- '--with-http_realip_module'
A matching build prints the module flag:
--with-http_realip_module
If the command returns no output, the installed build cannot use set_real_ip_from. Install a package that includes the module or rebuild Nginx with --with-http_realip_module before adding RealIP directives.
The correct header depends on the proxy layer directly in front of Nginx. Do not trust a header just because it exists; a direct internet client can send its own X-Forwarded-For or X-Real-IP header unless Nginx accepts it only from known proxy addresses.
| Front Proxy | Recommended Real IP Header | Trusted Source | Notes |
|---|---|---|---|
| Cloudflare HTTP proxy | CF-Connecting-IP | Official Cloudflare IPv4 and IPv6 ranges | Use CF-Connecting-IP unless an Enterprise account intentionally adds True-Client-IP; Cloudflare documents both as the same visitor address with different header names. |
| Private load balancer or reverse proxy | X-Forwarded-For or X-Real-IP | Private IPs or CIDRs for proxies you control | Use real_ip_recursive on when the forwarded chain contains several trusted proxy hops. |
| TCP load balancer with PROXY protocol | proxy_protocol | The load balancer addresses | Enable only when the listener receives PROXY protocol. Ordinary HTTP clients will fail on that listener. |
| No proxy in front of Nginx | None | None | Leave RealIP disabled because $remote_addr already represents the direct client. |
Never use broad trust entries such as
0.0.0.0/0or::/0for RealIP. That lets any client forge the apparent address used by logs, rate limits, deny rules, and applications that trust Nginx headers.
Configure Nginx Real IP for Cloudflare
Cloudflare needs two pieces in Nginx: every current Cloudflare origin-facing IP range in set_real_ip_from, and real_ip_header CF-Connecting-IP. Put these directives in an include file loaded from the http context so they apply consistently to all Cloudflare-proxied server blocks.
Cloudflare documents CF-Connecting-IP as the visitor address sent from its edge to your origin. Its IP ranges page is the source to trust for set_real_ip_from entries.
Cloudflare sends several request headers, but only a few are real client-IP sources for Nginx. Keep the default simple unless your account or upstream chain has a specific reason to use another header:
| Cloudflare Header or Mode | Nginx RealIP Use | When to Use It |
|---|---|---|
CF-Connecting-IP | real_ip_header CF-Connecting-IP; | Recommended default for normal Cloudflare HTTP or HTTPS proxy traffic because it contains a single visitor address. |
X-Forwarded-For | real_ip_header X-Forwarded-For; with careful trust ranges and usually real_ip_recursive on; | Supported by Cloudflare and Nginx, but better for controlled proxy chains than a simple Cloudflare-only origin because it can contain multiple addresses. |
True-Client-IP | real_ip_header True-Client-IP; | Enterprise-only Cloudflare option added through a Managed Transform. Cloudflare documents it as the same visitor address as CF-Connecting-IP, only under a legacy header name. |
CF-Connecting-IPv6 | Log or troubleshoot only | Use when Cloudflare Pseudo IPv4 overwrites visitor IP headers and you still need to see the real IPv6 address. |
CF-Pseudo-IPv4 | Do not use as the normal RealIP source | Compatibility value for IPv4-only origin software, not the actual visitor address for IPv6 clients. |
| Remove visitor IP headers Managed Transform | Do not enable for RealIP restoration | Use only when the origin should not receive visitor-IP headers. It removes or strips visitor-IP values from the headers RealIP depends on. |
Other Cloudflare headers such as CF-Ray, CF-IPCountry, X-Forwarded-Proto, or CF-Visitor | Not RealIP sources | Useful for logging, routing, country handling, or scheme detection, but they do not replace the client address in Nginx. |
Check which include paths your active Nginx configuration loads:
sudo nginx -T 2>/dev/null | awk '
/^# configuration file / { file=$0 }
/^[[:space:]]*include[[:space:]].*(conf\.d|sites-enabled)/ {
print file
print
}
'
Relevant output often includes /etc/nginx/conf.d/*.conf, /etc/nginx/sites-enabled/*, or both. The # configuration file line identifies the file that owns each include:
# configuration file /etc/nginx/nginx.conf:
include /etc/nginx/conf.d/*.conf;
# configuration file /etc/nginx/nginx.conf:
include /etc/nginx/sites-enabled/*;
The example path used here is /etc/nginx/conf.d/cloudflare-realip.conf, which works on many packaged Nginx layouts because conf.d is commonly included inside http. If your active configuration does not load conf.d from http, save the file in an include path that does.
Generate a Cloudflare RealIP include from the official IPv4 and IPv6 text endpoints. The block stages the new file first, checks that both downloads produced CIDR-shaped content, and backs up an existing include before replacing it:
(
set -euo pipefail
cf_tmp=$(mktemp -d)
trap 'rm -rf "$cf_tmp"' EXIT
curl -fsSL https://www.cloudflare.com/ips-v4 -o "$cf_tmp/ips-v4"
curl -fsSL https://www.cloudflare.com/ips-v6 -o "$cf_tmp/ips-v6"
if [ ! -s "$cf_tmp/ips-v4" ] || [ ! -s "$cf_tmp/ips-v6" ]; then
printf 'Cloudflare IP range download was empty.\n' >&2
exit 1
fi
awk '
/^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\/[0-9]+$/ {
v4++
print "set_real_ip_from " $0 ";"
next
}
/^[0-9A-Fa-f:]+\/[0-9]+$/ {
v6++
print "set_real_ip_from " $0 ";"
next
}
NF {
printf "Unexpected Cloudflare IP range line: %s\n", $0 > "/dev/stderr"
bad=1
}
END {
if (bad || v4 == 0 || v6 == 0) exit 1
print "real_ip_header CF-Connecting-IP;"
}
' \
"$cf_tmp/ips-v4" "$cf_tmp/ips-v6" > "$cf_tmp/cloudflare-realip.conf"
if [ -e /etc/nginx/conf.d/cloudflare-realip.conf ]; then
sudo cp -a /etc/nginx/conf.d/cloudflare-realip.conf \
/etc/nginx/conf.d/cloudflare-realip.conf.backup
fi
sudo install -m 0644 "$cf_tmp/cloudflare-realip.conf" \
/etc/nginx/conf.d/cloudflare-realip.conf
)
The subshell keeps the temporary directory cleanup local to this operation. If either Cloudflare download fails, returns an empty file, lacks an IPv4 or IPv6 range, or contains an unexpected non-CIDR line, the subshell stops before it writes to /etc/nginx/conf.d/cloudflare-realip.conf. The final sudo install -m 0644 command copies the staged include into place with normal root-owned config-file permissions; it is not a package installation step.
Inspect the generated directives before applying them:
sudo grep -E '^(set_real_ip_from|real_ip_header)' /etc/nginx/conf.d/cloudflare-realip.conf
The file should contain Cloudflare IPv4 ranges, Cloudflare IPv6 ranges, and one real_ip_header CF-Connecting-IP; line. If either address family is missing, remove the file and repeat the download after fixing network or DNS access.
Enterprise zones that intentionally add Cloudflare’s True-Client-IP header do not need a different trust list. Keep the same Cloudflare set_real_ip_from ranges and change only the header line in the include:
real_ip_header True-Client-IP;
Do not switch to True-Client-IP only because the account supports it. If the Managed Transform is disabled or removed, the selected header is absent and Nginx keeps the Cloudflare edge address as $remote_addr.
Test the Nginx configuration, then reload the service only after the syntax check passes:
sudo nginx -t
sudo systemctl reload nginx
A successful syntax test prints these lines before the reload:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful
Cloudflare’s IP ranges page is the definitive source for current ranges and includes update history. Regenerate the include file when that page changes, after a long maintenance gap, or when origin firewall rules begin blocking legitimate Cloudflare traffic. Keep the nginx -t check before reload so malformed downloads do not reach the running configuration.
Configure Real IP for a Private Reverse Proxy
For a private load balancer, HAProxy instance, container ingress, or another reverse proxy you control, trust only the proxy addresses that can connect directly to Nginx. Use X-Forwarded-For when the proxy appends a forwarding chain, and enable recursive lookup when intermediate proxy hops are also trusted.
set_real_ip_from 10.0.0.10;
set_real_ip_from 10.0.1.0/24;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
With real_ip_recursive on, Nginx walks the forwarded chain from right to left and chooses the last address that is not in a trusted proxy range. That keeps the application-facing client IP stable when traffic passes through more than one internal proxy before reaching the origin.
If your proxy overwrites X-Real-IP with a single client address instead of maintaining X-Forwarded-For, change the header source deliberately:
set_real_ip_from 10.0.0.10;
real_ip_header X-Real-IP;
Use the broader Nginx reverse proxy configuration workflow when the same server block also needs upstream routing, host headers, TLS termination, WebSocket support, or backend isolation.
Configure Real IP with PROXY Protocol
Some TCP load balancers send client connection metadata with PROXY protocol instead of an HTTP header. Nginx can read that metadata, but the listener must opt in with proxy_protocol before real_ip_header proxy_protocol can use it.
The official NGINX PROXY protocol documentation shows the same pairing: enable PROXY protocol on the listener, then use the RealIP module to replace the load balancer address with the client address.
For Cloudflare, PROXY protocol is a Spectrum TCP application case, not the normal setup for proxied HTTP and HTTPS DNS records. Cloudflare’s Spectrum PROXY protocol documentation describes this as a way to pass client IP details to an origin service. Normal Cloudflare HTTP proxy traffic sends request headers such as CF-Connecting-IP instead.
server {
listen 80 proxy_protocol;
listen [::]:80 proxy_protocol;
server_name example.com;
set_real_ip_from 10.0.0.10;
real_ip_header proxy_protocol;
location / {
proxy_pass http://127.0.0.1:3000;
}
}
Enable
proxy_protocolonly on listeners that always receive PROXY protocol from the upstream load balancer. A normal browser or health-check request sent directly to that listener will fail because Nginx expects the PROXY line before the HTTP request.
For HTTPS listeners, keep the same idea but combine the listener parameters in the order your Nginx version accepts, such as listen 443 ssl proxy_protocol;. Test with sudo nginx -t before reloading because listener syntax mistakes can take the virtual host offline.
Update Logs and Upstream Headers After Real IP
After RealIP is active, $remote_addr is the restored client address. Keep $realip_remote_addr in a custom log format when you still need to see which proxy connected to the origin.
log_format realip_combined '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'proxy="$realip_remote_addr" '
'xff="$http_x_forwarded_for" '
'cf="$http_cf_connecting_ip" '
'true="$http_true_client_ip" '
'cfv6="$http_cf_connecting_ipv6" '
'ray="$http_cf_ray"';
access_log /var/log/nginx/access.log realip_combined;
Place log_format in the http context before any access_log directive that references it. The Nginx access and error logs guide covers per-site log paths, log rotation, and troubleshooting when a request appears in a different file than expected.
If Nginx also proxies to an application backend, send the normalized restored address after the RealIP module runs. This default gives the backend one client address to trust from Nginx:
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
Use $proxy_add_x_forwarded_for only when the backend intentionally consumes the full forwarding chain and ignores untrusted left-side values, because it appends the restored address to the incoming chain. Application frameworks often have their own trusted-proxy setting; configure the application to trust the Nginx proxy address, not arbitrary internet clients, before using forwarded headers for login throttling, admin allowlists, redirects, or audit trails.
RealIP also affects Nginx rules that use client-address variables. For example, limit_req_zone $binary_remote_addr starts keying on the restored client address after RealIP is active, which is usually what you want behind Cloudflare. Review the Nginx rate limiting guide before changing production limits because one setting can shift enforcement from proxy IPs to individual visitors.
Verify Restored Client IPs in Nginx
Start with a configuration dump to confirm Nginx loaded the intended directives:
sudo nginx -T 2>/dev/null | awk '
/^# configuration file / { file=$0 }
/^[[:space:]]*(set_real_ip_from|real_ip_header|real_ip_recursive|log_format)/ {
print file
print
}
'
The output should show your trusted proxy ranges, the selected header, and any custom log format. If the directives are missing, the include file is not loaded from the active http, server, or location context.
Follow the access log with the tail command while you make one controlled request through the public proxy path:
sudo tail -f /var/log/nginx/access.log
From another terminal or another machine, send a normal request through the domain:
curl -sS -o /dev/null -D - https://example.com/ | sed -n '1p'
A successful request prints a normal status line, such as:
HTTP/2 200
In the access log, the first address should be the visitor address after RealIP replacement. If you used the custom format, proxy="..." should show the Cloudflare edge, load balancer, or reverse proxy address that connected directly to Nginx.
Use the curl command for repeatable request checks, but avoid proving Cloudflare behavior with a direct origin request from your workstation. A direct request does not come from a Cloudflare range, so Nginx should ignore any forged CF-Connecting-IP header on that connection.
A direct-origin probe is still useful as a spoofing check. When you know the origin IP and can safely reach it, preserve the public host and TLS name with --resolve while sending a forged header:
origin_ip=203.0.113.10
curl --noproxy '*' --resolve "example.com:443:$origin_ip" \
-H 'CF-Connecting-IP: 203.0.113.77' \
-sS -o /dev/null -D - https://example.com/ | sed -n '1p'
The status line only proves that the origin answered. The access log should still show the direct source address or a trusted proxy address, not 203.0.113.77. If a request that does not come from a trusted proxy changes $remote_addr to a forged header value, a set_real_ip_from entry is too broad and should be removed before you rely on logs, rate limits, or application trust rules.
Roll Back Nginx Real IP Changes
Roll back RealIP changes if logs become misleading, an upstream proxy is not sending the header you expected, or a syntax issue blocks reloads. Remove only the file or directives you created for this change.
For the Cloudflare include file at /etc/nginx/conf.d/cloudflare-realip.conf, restore the backup created by the generation block when it exists. If no backup exists, remove only the include file created for this change:
if [ -f /etc/nginx/conf.d/cloudflare-realip.conf.backup ]; then
sudo cp -a /etc/nginx/conf.d/cloudflare-realip.conf.backup \
/etc/nginx/conf.d/cloudflare-realip.conf
else
sudo rm -f /etc/nginx/conf.d/cloudflare-realip.conf
fi
sudo nginx -t && sudo systemctl reload nginx
If you edited /etc/nginx/nginx.conf directly, restore the backup you made before the change:
sudo cp /etc/nginx/nginx.conf.backup /etc/nginx/nginx.conf
sudo nginx -t && sudo systemctl reload nginx
After rollback, $remote_addr returns to the direct peer address. Behind Cloudflare or another proxy, that means logs will again show the proxy address until RealIP is repaired.
Troubleshoot Nginx Real IP Problems
Nginx Reports Unknown Directive set_real_ip_from
This error means the installed Nginx build does not include the HTTP RealIP module. Confirm the build flags first:
nginx -V 2>&1 | tr ' ' '\n' | grep -- '--with-http_realip_module'
If no module flag appears, switch to a packaged Nginx build that includes RealIP or rebuild Nginx with the module enabled. Do not keep RealIP directives in the active configuration until the module exists.
Logs Still Show Cloudflare or Proxy IPs
If logs still show proxy addresses, Nginx either did not load the RealIP directives, did not trust the direct proxy address, or read the wrong header. Inspect the active configuration first:
sudo nginx -T 2>/dev/null | awk '
/^# configuration file / { file=$0 }
/^[[:space:]]*(set_real_ip_from|real_ip_header|real_ip_recursive)/ {
print file
print
}
'
For Cloudflare, confirm the request reaches the origin through a proxied DNS record and that the source address belongs to Cloudflare’s published ranges. If the expected visitor header is missing, check Cloudflare Transform Rules and Managed Transforms; the Remove visitor IP headers transform removes CF-Connecting-IP and True-Client-IP, and can strip the visitor address from X-Forwarded-For in stacked-CDN traffic. For a private proxy, confirm the direct proxy IP matches one of your set_real_ip_from entries and that the proxy sends the header named by real_ip_header.
Nginx Fails with Directive Is Not Allowed Here
The RealIP directives belong in http, server, or location context. A standalone file included at the top level of nginx.conf can trigger a context error even when the same file would work inside http.
Find the include line that loads the file:
sudo grep -R --line-number -e 'cloudflare-realip' -e 'set_real_ip_from' /etc/nginx/
Move the RealIP include into the active http block or place the directives directly inside the affected server block. Retest with sudo nginx -t before reloading.
Clients Can Spoof X-Forwarded-For
Spoofing becomes possible when Nginx trusts public clients or a proxy passes client-supplied headers without overwriting them. Search the active config for broad trust entries:
sudo nginx -T 2>/dev/null | awk '
/^# configuration file / { file=$0 }
/^[[:space:]]*set_real_ip_from[[:space:]]+(0\.0\.0\.0\/0|::\/0)/ {
print file
print
}
'
Remove any broad trust entry and replace it with exact proxy IPs or provider ranges. Also restrict direct origin access where possible so public clients cannot bypass Cloudflare, the load balancer, or the trusted reverse proxy layer. If you choose an Nginx-layer restriction, use the Nginx allow and block IP address workflow; a firewall or cloud security group is usually cleaner when it can enforce the same proxy ranges before traffic reaches Nginx.
Backend Applications Still See the Proxy IP
Nginx may be restoring $remote_addr correctly while the backend application still ignores forwarded headers. Confirm the proxy location sends the restored address:
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
Then configure the backend framework to trust the Nginx proxy address. Keep that trust list narrow; applications that trust forwarded headers from every source can make authentication logs, rate limits, redirects, and admin IP restrictions depend on forged client values.
Cloudflare IPv6 Visitors Show Pseudo IPv4 Values
Cloudflare’s Pseudo IPv4 setting can overwrite CF-Connecting-IP and X-Forwarded-For with a generated IPv4 value while preserving the real IPv6 address in CF-Connecting-IPv6. If IPv6 accuracy matters, check the Cloudflare Network settings and add $http_cf_connecting_ipv6 to a temporary log format while you confirm the behavior.
Conclusion
Nginx can log and forward real client IPs when the RealIP module trusts only the proxy layer that actually connects to the origin. Keep Cloudflare ranges current, preserve $realip_remote_addr for proxy visibility, and pair the change with focused Nginx log checks before adjusting rate limits, deny rules, or backend trust settings.


Formatting tips for your comment
You can use basic HTML to format your comment. Useful tags currently allowed in published comments:
<code>command</code>command<strong>bold</strong><em>italic</em><a href="https://example.com">link</a><blockquote>quote</blockquote>