Reverse Proxy Headers & Context Preservation

Nginx In Depth

Chapter 6 · Reverse Proxy Headers & Context Preservation

Web Servers Fundamentals Chapter 6 showed proxy_pass forwarding a request to a backend, but never addressed what the backend actually sees once that happens. By default, it's less than you'd expect — and fixing that is this chapter's whole job.

The Problem: The Backend Only Sees Nginx

Once Nginx proxies a request, the TCP connection the backend application actually receives comes from Nginx's own IP address, not the real client's. Left unaddressed, this silently breaks anything at the application layer that depends on knowing the real client — IP-based rate limiting, geolocation, access logs that should show real visitor IPs — because as far as the backend can tell, every single request came from the same one machine: Nginx itself.

X-Forwarded-For

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

X-Forwarded-For is the de facto standard header carrying the original client's IP address to the backend. Using Nginx's own $proxy_add_x_forwarded_for variable — rather than just $remote_addr — matters specifically when a request passes through more than one proxy hop: it appends the current client IP to any existing X-Forwarded-For value already on the request, building a comma-separated chain, rather than overwriting whatever a previous proxy already added.

X-Forwarded-Proto and X-Forwarded-Host

proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host;

X-Forwarded-Proto tells the backend whether the original client's request was HTTP or HTTPS — critical when TLS is terminated at Nginx itself (Web Servers Fundamentals Chapter 7), since Nginx's own internal connection to the backend is very often plain HTTP regardless of what the original client used. Without this header, a backend can incorrectly conclude the whole request was insecure, which can break secure-cookie logic or redirect behavior that depends on knowing HTTPS was actually used. X-Forwarded-Host similarly preserves the original Host header the client actually requested, in case Nginx's own internal request to the backend uses a different hostname.

X-Real-IP

proxy_set_header X-Real-IP $remote_addr;

X-Real-IP, popularized by Nginx itself, is a simpler alternative to X-Forwarded-For: just the immediate client's IP address as a single value, with no comma-separated chain to parse. Many applications support reading either header; X-Real-IP is convenient specifically when there's exactly one proxy hop and no chain to represent.

proxy_set_header

proxy_set_header is the general directive underlying every example above — it sets (or overrides) a header on the request Nginx sends to the backend. It's worth knowing this directive doesn't behave like some others when nested inside a location block, which the next section covers directly.

HeaderPurpose
X-Forwarded-ForChain of client IPs across multiple proxy hops
X-Real-IPThe immediate client's single IP address
X-Forwarded-ProtoWhether the original request was HTTP or HTTPS
X-Forwarded-HostThe original Host header the client requested
Closing the gap Web Servers Fundamentals raised
Web Servers Fundamentals Chapter 7's own warning box flagged that terminating TLS at the web server doesn't automatically mean the hop to a backend stays encrypted — and that a backend has no automatic way of knowing the original connection was HTTPS at all. X-Forwarded-Proto is precisely the mechanism that closes that specific gap, letting the backend correctly recognize a request as having arrived over HTTPS even though its own direct connection to Nginx is plain HTTP.
A location block with its own proxy_set_header stops inheriting ALL of them
This is a well-known and easy-to-miss Nginx gotcha: if an outer server {} block sets several proxy_set_header directives, and a nested location {} block then sets even one proxy_set_header of its own, that location block stops inheriting every proxy_set_header from the parent context — not just the one it explicitly overrode. Every header actually needed inside that location block then has to be repeated there explicitly, or it silently disappears from requests that go through it, with no error or warning to flag the omission.

Hands-On Exercises

Exercise 1

Write the three proxy_set_header directives needed so a backend receives the original client's IP address chain, the original protocol (HTTP or HTTPS), and the original Host header.

📄 View solution
Exercise 2

A backend application redirects users to an insecure http:// URL even though every client reaches the site over HTTPS, with TLS terminated at Nginx. Using this chapter's own material, explain the likely missing piece and how to fix it.

📄 View solution
Exercise 3

A server {} block sets X-Forwarded-For and X-Forwarded-Proto. A location /api/ {} block inside it adds one more header, X-Api-Version, via its own proxy_set_header. Explain what happens to X-Forwarded-For and X-Forwarded-Proto for requests handled by that location block, and how to fix it.

📄 View solution

Chapter 6 Quick Reference

  • By default, a proxied backend only sees Nginx's own IP, not the real client's — breaking anything that depends on the real client identity
  • X-Forwarded-For — client IP chain via $proxy_add_x_forwarded_for; X-Real-IP — single immediate client IP
  • X-Forwarded-Proto/X-Forwarded-Host — preserve the original scheme and Host header, closing the gap left by TLS termination at Nginx
  • A location block with its own proxy_set_header stops inheriting ALL parent headers, not just the one it overrides — every needed header must be repeated there explicitly