Real client ip for Magento 2

A clean, modern vector style on a plain white backdrop: a gently curving path with multiple stops along the route, each stop containing a small circle labeled "IP" to symbolize searching for the real IP. The stops use distinct pastel colors and subtle shadows to stand out against the white background

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.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *