1. Introduction

The X-Forwarded-For (XFF) header tells your application the real client IP address when requests pass through one or more proxies. When it's not working, your application logs and rate limiters see load balancer or pod IPs instead of real visitor IPs — breaking geo-blocking, analytics, security controls, and any feature that depends on knowing who made a request.

This guide covers why XFF breaks in Nginx, AWS ALB/NLB, and Kubernetes Ingress environments, and how to fix each one so your application reliably receives the real client IP.

2. What X-Forwarded-For Is and Why It Breaks

When a client makes a request through a reverse proxy, the proxy typically adds an X-Forwarded-For header containing the client's IP. If there are multiple proxies (client → CDN → load balancer → Nginx → application), each proxy appends to the header: X-Forwarded-For: <client-ip>, <cdn-ip>, <lb-ip>. Your application reads the first value (leftmost) to get the original client IP.

This breaks when: (1) a proxy doesn't forward the header, (2) a proxy overwrites the header instead of appending, (3) the application reads the wrong position in the header, or (4) the header contains untrusted values injected by the client.

3. Common Causes

4. Step-by-Step Fix

Step 1: Confirm what headers the application is receiving

# Deploy a debug pod that echoes request headers
kubectl run header-debug --image=mendhak/http-https-echo --rm -it   --restart=Never --port=8080 --expose

# In another terminal, curl through your Ingress:
curl -s https://your-ingress-host/
# The response body will show all request headers including X-Forwarded-For

# Or use httpbin:
kubectl run httpbin --image=kennethreitz/httpbin --rm -it   --restart=Never --port=80 --expose
curl https://your-ingress-host/ip
# Returns: {"origin": "..."} — should show real client IP

Step 2: Fix — Nginx reverse proxy configuration

server {
    listen 80;

    location / {
        proxy_pass http://backend;

        # Pass the real client IP
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $host;

        # Do NOT use $remote_addr for X-Forwarded-For if Nginx is behind another proxy
        # $proxy_add_x_forwarded_for appends to existing XFF header
        # $remote_addr would just set it to the upstream proxy IP
    }
}

# If Nginx is behind a trusted proxy (ALB, Cloudflare, etc.)
# use the realip module to set $remote_addr from the XFF header:
set_real_ip_from 10.0.0.0/8;         # trust your ALB subnet
real_ip_header X-Forwarded-For;
real_ip_recursive on;

Step 3: Fix — AWS ALB target type

AWS ALB always adds an X-Forwarded-For header with the real client IP. Your application should read this header rather than the connection's remote IP (which will be the ALB's IP). See also Fix AWS Load Balancer Not Routing Traffic for related ALB configuration issues.

# For ALB in IP target type (direct pod targeting):
# The pod receives requests from the ALB directly
# ALB sets X-Forwarded-For to the original client IP
# Your app should read: request.headers['X-Forwarded-For'].split(',')[0].strip()

# In your application config, trust the ALB's subnet:
# Express.js example:
app.set('trust proxy', '10.0.0.0/8')  # your ALB subnet CIDR
# After this, req.ip will return the real client IP from XFF

# Django example (settings.py):
USE_X_FORWARDED_HOST = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# Add to MIDDLEWARE: 'django.middleware.common.CommonMiddleware'

Step 4: Fix — AWS NLB Proxy Protocol

Unlike ALBs, NLBs operate at Layer 4 (TCP) and don't modify HTTP headers. To pass the real client IP through an NLB, enable Proxy Protocol v2 on the NLB target group. Your backend (Nginx, HAProxy, etc.) must then be configured to read Proxy Protocol headers.

# Enable Proxy Protocol on NLB target group via AWS CLI
aws elbv2 modify-target-group-attributes   --target-group-arn arn:aws:elasticloadbalancing:...   --attributes Key=proxy_protocol_v2.enabled,Value=true

# Configure Nginx to accept Proxy Protocol on the listening port
server {
    listen 80 proxy_protocol;
    listen 443 ssl proxy_protocol;

    # Set real_ip from proxy protocol
    real_ip_header proxy_protocol;
    set_real_ip_from 0.0.0.0/0;

    # Now $remote_addr contains the real client IP
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $remote_addr;
}

Step 5: Fix — Kubernetes NGINX Ingress Controller

# The NGINX Ingress Controller passes XFF by default for HTTP/HTTPS
# Verify it's not being stripped with a ConfigMap annotation:
kubectl get configmap ingress-nginx-controller -n ingress-nginx -o yaml |   grep -E "use-forwarded-headers|forwarded-for-header|compute-full-forwarded-for"

# To ensure real IPs pass through to backends, add to the controller ConfigMap:
apiVersion: v1
kind: ConfigMap
metadata:
  name: ingress-nginx-controller
  namespace: ingress-nginx
data:
  use-forwarded-headers: "true"
  compute-full-forwarded-for: "true"
  # If behind a trusted proxy (e.g. Cloudflare or ALB):
  proxy-real-ip-cidr: "10.0.0.0/8"   # your upstream proxy CIDR

# Apply and restart the controller
kubectl rollout restart deployment/ingress-nginx-controller -n ingress-nginx

Step 6: Fix — Application layer

# If Nginx and proxies are correct but the app still shows wrong IP,
# the application is reading the wrong value.

# Node.js / Express:
app.set('trust proxy', 1)  # trust first proxy
console.log(req.ip)        # now shows real client IP

# Python / Flask:
from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)

# The raw header value (read first entry for original client):
real_ip = request.headers.get('X-Forwarded-For', '').split(',')[0].strip()

# Go / Gin:
router.TrustedProxies([]string{"10.0.0.0/8"})
c.ClientIP()  # reads XFF from trusted proxies

5. Verification Steps

# Verify XFF header reaches the application with the correct IP
# Use a tool that echoes headers:
curl -H "X-Forwarded-For: 1.2.3.4" https://your-ingress-host/headers
# Your application should see X-Forwarded-For: 1.2.3.4, <real-proxy-ip>

# Check Nginx access log format includes real IP
# In nginx.conf:
log_format main '$http_x_forwarded_for - $remote_addr - $request';
access_log /var/log/nginx/access.log main;

# Test from a known external IP:
tail -f /var/log/nginx/access.log
# First column should show your real external IP, not the ALB IP

6. Common Mistakes

7. Prevention Tips

8. FAQ

I see the correct IP in X-Forwarded-For but my app still shows the wrong IP. Why?

Your application isn't reading the XFF header — it's using the connection's remote IP address instead. This is a very common framework configuration issue. In Express, set app.set('trust proxy', 1). In Django, ensure ProxyFix middleware is active. In most frameworks, there's a "trust proxy" setting that must be explicitly enabled before the XFF header is used for req.ip or equivalent.

X-Forwarded-For has three IPs. Which one is the real client?

The first (leftmost) IP is the original client IP, assuming the outermost proxy correctly set the header. Format is: X-Forwarded-For: <client>, <proxy1>, <proxy2>. Only trust the first value if your outermost proxy (CDN, ALB) has been configured to set it from the actual TCP connection — not from a client-supplied header.

Does AWS ALB support X-Forwarded-For automatically?

Yes. AWS ALB always adds X-Forwarded-For with the real client IP. You don't need any special configuration on the ALB side. Your backend just needs to read this header. If you're using Nginx behind the ALB, configure set_real_ip_from with your ALB subnet CIDR so Nginx knows to trust the header from that source.

9. Summary

ScenarioReal IP mechanismFix
Nginx proxyproxy_set_header X-Forwarded-For $proxy_add_x_forwarded_forAdd to proxy location block; add realip module config
AWS ALBALB auto-adds XFF headerConfigure app to trust XFF; set trust proxy in framework
AWS NLB (TCP)Proxy Protocol v2Enable PP on target group; configure Nginx to read PP
K8s NGINX IngressController ConfigMap settingSet use-forwarded-headers: true in controller ConfigMap
Application reading wrong valueFramework trust proxy settingEnable trust proxy / ProxyFix in application framework

Explore More in This Category

Explore more in this category: Networking & Security guides. Browse all DevOps Compass articles or jump to: Kubernetes, AWS, CI/CD, Containers, Monitoring, Networking.