When setting up Nginx as a reverse proxy, I encountered a common issue where backend servers only see the proxy server's IP address instead of the actual client IP. This breaks functionality like:
- Geo-IP based content delivery
- IP-based rate limiting
- Security logging and analytics
In my configuration, the traffic flow looks like:
Client → Nginx Proxy → Apache/PHP Backend
Three critical headers need proper handling:
- X-Real-IP
- X-Forwarded-For
- REMOTE_ADDR (PHP's default)
Here's the working solution I implemented after several trials:
server {
listen 80;
server_name example.com;
# Trust all proxies (adjust for production)
set_real_ip_from 0.0.0.0/0;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
location / {
proxy_pass http://backend_server;
proxy_set_header Host $host;
# Critical IP forwarding headers
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Additional optimization parameters
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off;
}
}
For PHP applications, modify your code to check the forwarded headers:
<?php
function getClientIP() {
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
return $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
return trim($ips[0]);
} else {
return $_SERVER['REMOTE_ADDR'];
}
}
echo "Client IP: " . getClientIP();
?>
Important precautions when implementing this:
- Restrict
set_real_ip_from
to known proxy IPs - Validate IP addresses in your application code
- Consider using the PROXY protocol for TCP services
- Implement rate limiting at both Nginx and application layers
When setting up an Nginx reverse proxy, many developers encounter the issue where backend servers only see the proxy server's IP address instead of the original client IP. This breaks geo-targeting, logging, and security features that depend on client IP information.
Your current configuration is close but needs a few critical adjustments. Here's the corrected version:
server {
listen 80;
server_name example.com;
# Real IP configuration
set_real_ip_from 0.0.0.0/0;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
location / {
proxy_pass http://backend_server;
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;
}
}
- Changed
real_ip_header
from X-Real-IP to X-Forwarded-For - Added
real_ip_recursive on
to properly parse chained proxies - Included X-Forwarded-Proto header for HTTPS awareness
For PHP applications, you'll need to modify your code to read the correct header:
<?php
function getRealClientIP() {
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
return $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
return trim($ips[0]);
}
return $_SERVER['REMOTE_ADDR'];
}
echo getRealClientIP();
?>
Create a test endpoint that displays all relevant headers:
<?php
echo "REMOTE_ADDR: " . $_SERVER['REMOTE_ADDR'] . "\n";
echo "HTTP_X_REAL_IP: " . ($_SERVER['HTTP_X_REAL_IP'] ?? 'Not set') . "\n";
echo "HTTP_X_FORWARDED_FOR: " . ($_SERVER['HTTP_X_FORWARDED_FOR'] ?? 'Not set') . "\n";
?>
When accepting forwarded IP addresses:
- Limit
set_real_ip_from
to only trusted proxy IPs - Validate IP formats before using them
- Consider rate limiting based on X-Forwarded-For
If you're still seeing the proxy IP:
- Verify the headers reach your backend (check access logs)
- Ensure no intermediate proxies strip headers
- Check for conflicting Nginx modules