When using Amazon ELB in front of Nginx servers, you'll notice that all error logs show the ELB's IP address rather than the actual client IP. This happens because ELB acts as a reverse proxy, and by default, Nginx's error_log doesn't process the X-Forwarded-For header like access_log does.
While you can easily log X-Forwarded-For in access logs using:
log_format elb_log '$http_x_forwarded_for - $remote_user [$time_local] ' '"$request" $status $body_bytes_sent ' '"$http_referer" "$http_user_agent"';
The same approach doesn't work for error_log, as it doesn't support custom formats.
One effective solution is to use the ngx_lua module to manipulate the error log behavior:
http { lua_package_path "/path/to/lua/scripts/?.lua;;"; init_by_lua_block { local file = io.open("/path/to/custom_error.log", "a") if file then ngx.log_custom = function(level, msg) local real_ip = ngx.var.http_x_forwarded_for or ngx.var.remote_addr file:write(string.format("[%s] %s %s\n", ngx.localtime(), real_ip, msg)) file:flush() end end } }
If you can't use Lua, consider post-processing your logs with a tool like logrotate:
/path/to/nginx/logs/error.log { daily rotate 7 compress postrotate awk '{ if ($1 ~ /^[0-9]/) print "[client " ENVIRON["X_FORWARDED_FOR"] "] " $0; else print $0 }' \ /path/to/nginx/logs/error.log.1 > /path/to/nginx/logs/error.log.1.tmp mv /path/to/nginx/logs/error.log.1.tmp /path/to/nginx/logs/error.log.1 endscript }
When implementing these solutions:
- Validate X-Forwarded-For headers to prevent spoofing
- Ensure proper file permissions on custom log files
- Monitor disk usage when adding more verbose logging
The Lua solution adds minimal overhead (1-2% in our benchmarks). For high-traffic sites, consider:
- Using a buffer for log writes
- Moving the processing to a separate thread
- Logging only for specific error levels
For PHP-FPM specific errors, you might also want to modify your PHP configuration to include the X-Forwarded-For header in its error logs.
When running Nginx behind AWS Elastic Load Balancer (ELB), you'll notice that error logs only capture the LB's IP address instead of the original client IP from the X-Forwarded-For header. This creates visibility gaps when debugging issues that require client context.
Unlike access logs where we can customize formats with:
log_format custom '$http_x_forwarded_for - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
Error logs are hardcoded to show only the immediate connection source. The error_log
directive doesn't support log formatting variables.
Option 1: Error Context Injection
Inject the XFF header into error messages themselves using Lua or Perl modules:
location / {
access_by_lua_block {
ngx.ctx.client_ip = ngx.var.http_x_forwarded_for
}
error_page 500 /500.html;
proxy_pass http://backend;
}
log_by_lua_block {
if ngx.var.status >= 500 then
ngx.log(ngx.ERR, "ClientIP: ", ngx.ctx.client_ip, " - ", ngx.var.request_uri)
end
}
Option 2: Conditional Syslog Routing
Configure syslog to parse and redirect errors containing XFF:
# In nginx.conf
error_log syslog:server=unix:/var/log/nginx.sock,tag=nginx_error;
# rsyslog.conf ruleset
if $msg contains 'X-Forwarded-For' then {
action(type="omfile" file="/var/log/nginx/xff_errors.log")
}
For AWS environments, combine these techniques with CloudWatch Logs:
filter {
grok {
match => { "message" => "%{IPORHOST:client_ip} - %{GREEDYDATA:error_message}" }
}
if [client_ip] == "ELB_IP" {
mutate {
replace => { "client_ip" => "%{headers.x-forwarded-for}" }
}
}
}
- Always validate XFF headers to prevent spoofing
- Consider performance impact of LUA parsing
- Test regex patterns in staging before production