Skip to content

Forwarded Header (RFC 7239)

Traditionally, HTTP reverse proxies use non-standard headers:

X-Forwarded-For: 12.34.56.78, 23.45.67.89
X-Real-IP: 12.34.56.78
X-Forwarded-Host: example.com
X-Forwarded-Proto: https

RFC 7239 standardizes the Forwarded header:

Forwarded: for=12.34.56.78;host=example.com;proto=https, for=23.45.67.89

Benefits

The Forwarded header is extensible. For example, your trusted proxy can include a secret token:

Forwarded: for=12.34.56.78, for=23.45.67.89;secret=egah2CGj55fSJFs, for=10.1.2.3

NGINX Configuration

NGINX doesn't provide a built-in $proxy_add_forwarded variable, but you can emulate it:

map $remote_addr $proxy_forwarded_elem {
    # IPv4 addresses can be sent as-is
    ~^[0-9.]+$          "for=$remote_addr";

    # IPv6 addresses need to be bracketed and quoted
    ~^[0-9A-Fa-f:.]+$   "for=\"[$remote_addr]\"";

    # Unix domain socket names cannot be represented in RFC 7239 syntax
    default             "for=unknown";
}

map $http_forwarded $proxy_add_forwarded {
    # If the incoming Forwarded header is syntactically valid, append to it
    "~^(,[ \\t]*)*([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?(;([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?)*([ \\t]*,([ \\t]*([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?(;([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?)*)?)*$" "$http_forwarded, $proxy_forwarded_elem";

    # Otherwise, replace it
    default "$proxy_forwarded_elem";
}

Then in your proxy_pass block:

proxy_set_header Forwarded $proxy_add_forwarded;

You can also append extra parameters:

proxy_set_header Forwarded "$proxy_add_forwarded;proto=$scheme";

Multiple Headers Limitation

This solution does not support multiple incoming Forwarded headers. Unlike $proxy_add_x_forwarded_for, it won't correctly join:

Forwarded: for=1.2.3.4
Forwarded: for=5.6.7.8

Dealing with Invalid Headers

The complex regex above validates the Forwarded header syntax. Without validation, an attacker could send:

Forwarded: for=injected;by="

Which would produce:

Forwarded: for=injected;by=", for=real

If you trust your upstream to handle malformed headers, use a simpler config:

map $http_forwarded $proxy_add_forwarded {
    ""      "$proxy_forwarded_elem";
    default "$http_forwarded, $proxy_forwarded_elem";
}

Coexistence with X-Forwarded-*

You can either pass both headers:

proxy_set_header Forwarded $proxy_add_forwarded;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

Or remove legacy headers to avoid confusion:

proxy_set_header Forwarded $proxy_add_forwarded;
proxy_set_header X-Forwarded-For "";