How I made Magento 2 see the real client IP behind HAProxy + Varnish + Nginx (with Cloudflare)
If you run Magento 2 behind multiple layers, it’s common to lose the original client IP and end up with your proxy or load balancer address everywhere (logs, admin actions, rate-limiters, captchas). Here’s the setup that finally gave me the real IP all the way to PHP and Magento:
- HAProxy: add X-Forwarded-For
- Varnish: normalize X-Forwarded-For (favor Cloudflare CF-Connecting-IP if present; otherwise keep chain or fall back to client.ip)
- Nginx: trust the Varnish IP and use X-Forwarded-For to set the real client IP, then pass that header to PHP-FPM
Architecture
Client (optionally Cloudflare) -> HAProxy -> Varnish -> Nginx -> PHP-FPM (Magento 2)
Step 1: HAProxy — add the client IP once
In your HAProxy frontend that handles HTTP/HTTPS:
frontend fe_http
bind :80
option forwardfor if-none
default_backend be_varnish
frontend fe_https
bind :443 ssl crt /etc/haproxy/certs
option forwardfor if-none
http-request set-header X-Forwarded-Proto https if { ssl_fc }
default_backend be_varnish
backend be_varnish
server varnish 127.0.0.1:6081
Notes:
- option forwardfor adds X-Forwarded-For with the client source IP and, with if-none, won’t overwrite an existing header (e.g., from Cloudflare).
- Set X-Forwarded-Proto so downstream knows the original scheme.
Step 2: Varnish — normalize X-Forwarded-For
I used this in vcl_recv to prefer Cloudflare’s CF-Connecting-IP when present, otherwise keep what we already have, and finally fall back to client.ip:
if (req.http.CF-Connecting-IP) {
set req.http.X-Forwarded-For = req.http.CF-Connecting-IP;
} else if (req.http.X-Forwarded-For) {
# keep existing header (CF/HAProxy might have set it)
set req.http.X-Forwarded-For = req.http.X-Forwarded-For;
} else {
set req.http.X-Forwarded-For = client.ip;
}
Why this order:
- When using Cloudflare, CF-Connecting-IP is the canonical client address and is safer than trusting a possibly client-spoofed X-Forwarded-For arriving at the edge.
- If you’re not on Cloudflare, HAProxy will have added X-Forwarded-For, so we preserve it.
- If neither is set, we fall back to the direct client.ip seen by Varnish.
Optional in Varnish: ensure X-Forwarded-Proto is forwarded correctly as well (if HAProxy set it, just pass it through).
Step 3: Nginx — trust Varnish and apply the real IP
Tell Nginx which upstream IPs are trusted to provide X-Forwarded-For and then use that header as the source of truth. Also, pass the header through to PHP-FPM.
In http { }:
# Trust only your proxy chain. At minimum, trust Varnish:
set_real_ip_from 127.0.0.1; # or your Varnish host/IP
real_ip_header X-Forwarded-For;
real_ip_recursive on;
# Optional: useful for debugging
log_format main '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" xff="$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
Inside your PHP location block:
location ~ \.php$ {
include fastcgi_params;
# Make sure PHP sees the normalized header
fastcgi_param HTTP_X_FORWARDED_FOR $http_x_forwarded_for;
# Usual PHP-FPM pass
fastcgi_pass unix:/run/php/php-fpm.sock;
}
Critical pieces:
- set_real_ip_from must include your Varnish IP(s). Only trust known proxies. If Nginx also sits behind HAProxy directly in some paths, add those IPs too.
- real_ip_header X-Forwarded-For makes Nginx rewrite $remote_addr to the real client IP, using the trusted chain.
- real_ip_recursive on walks the header from right to left until it finds the first untrusted address, which becomes the real IP.
- Passing HTTP_X_FORWARDED_FOR to PHP ensures Magento/PHP code that inspects that header can still see the chain (while REMOTE_ADDR becomes the real client IP thanks to real_ip_header).
Magento 2 impact
- PHP’s $_SERVER[‘REMOTE_ADDR’] now shows the actual client address (thanks to Nginx real_ip_module).
- $_SERVER[‘HTTP_X_FORWARDED_FOR’] is available as the normalized chain (thanks to the fastcgi_param line).
- Admin actions, logs, rate-limiters, and security modules that rely on REMOTE_ADDR behave correctly.
- If you use Cloudflare, you’ll get the exact visitor IP via CF-Connecting-IP promoted into X-Forwarded-For at the Varnish layer.
Verification checklist
- From the shell on your web host:
- tail -f /var/log/nginx/access.log and confirm $remote_addr shows the real IP and xff shows what you expect.
- In Varnish:
- varnishlog -g request -i ReqHeader:X-Forwarded-For and confirm the header holds the correct client IP.
- In PHP:
- Create a small phpinfo() or var_dump($_SERVER) page temporarily and confirm REMOTE_ADDR equals the client IP and HTTP_X_FORWARDED_FOR is set.
- In Magento admin:
- Check recent logs, orders, or security modules for real client IPs.
Common pitfalls
- Trust only your proxies. Never set set_real_ip_from 0.0.0.0/0. That allows clients to spoof X-Forwarded-For.
- If you’re on Cloudflare and want to trust CF directly in Nginx, you can set real_ip_header CF-Connecting-IP and add all Cloudflare IP ranges to set_real_ip_from. The Varnish normalization shown above is often simpler to maintain.
- Ensure HAProxy uses option forwardfor if-none so it doesn’t stomp a legitimate X-Forwarded-For coming from an upstream like Cloudflare.
- Don’t forget to pass HTTP_X_FORWARDED_FOR to PHP-FPM:
fastcgi_param HTTP_X_FORWARDED_FOR $http_x_forwarded_for;
That’s it. With HAProxy setting X-Forwarded-For, Varnish normalizing it, and Nginx trusting only the Varnish IP and passing the header to PHP, Magento 2 finally sees the real visitor IP end-to-end.
Leave a Reply