A Spring app on Tomcat sits behind a TLS-terminating reverse proxy. It logs the proxy's IP address as every user's address and builds http:// links even though users arrived over HTTPS. Which Tomcat-side configuration fixes the client IP and scheme, and how do you keep an attacker from spoofing it?
answer
- the socket peer really is the proxy
- a valve restates the request first
- the header is a list, read right to left
- trust is a configured range, not a hope
- the edge must overwrite, not append
basics
~20 sEnable Tomcat's RemoteIpValve so forwarded headers rewrite the request's remote address, scheme, port and secure flag; restrict trust with its internalProxies and trustedProxies patterns. A static alternative is the connector's scheme, secure, proxyName and proxyPort attributes.
solid answer
~50 sTomcat sees the truth about its own socket: the peer really is the proxy, and the connection really is plaintext, so `getRemoteAddr()` and `getScheme()` are correct and useless. `RemoteIpValve` (or the equivalent `RemoteIpFilter`) fixes that by consuming the forwarded headers — `remoteIpHeader`, `x-forwarded-for` by default, and `protocolHeader`, typically set to `x-forwarded-proto` — and rewriting the request's remote address, scheme, server port and `isSecure()` before your code or the access log ever sees it. Trust is the whole game: the valve's `internalProxies` regular expression defines which immediate peers may be believed, and it walks the forwarded chain from the right, discarding entries contributed by trusted hops and taking the first untrusted address as the client. Because anyone can send `X-Forwarded-For`, the proxy must overwrite rather than append the client-supplied value, and `internalProxies` must not be widened to something that matches the internet. If nothing dynamic is needed, the connector's static `scheme="https" secure="true" proxyName proxyPort` attributes fix the URL construction, but not the client IP.
go deeper
Know that behind a proxy the app sees the proxy's address and a plaintext scheme, and that Tomcat has a valve which restores the original client address and scheme.
Name RemoteIpValve and its remoteIpHeader and protocolHeader settings, explain the four request properties it rewrites, and contrast it with the static connector scheme/secure/proxyName attributes.
Own the trust model: the right-to-left walk, what internalProxies must and must not match, and the requirement that the edge proxy overwrite a client-supplied header before any of it can be believed.
Decide where in the platform forwarded-header translation happens once and for all, so no service double-handles it, and treat client-IP trust as a security control with an owner rather than a per-service configuration accident.
## Why the app is not wrong, just uninformed Everything Tomcat reports is accurate about the connection it actually has. Its peer is the reverse proxy, so `request.getRemoteAddr()` returns the proxy's address. The proxy terminated TLS and forwarded plaintext to port 8080, so `getScheme()` returns `http`, `isSecure()` returns false, and `getServerPort()` returns 8080. Every absolute URL the framework builds — redirects, password-reset links, canonical tags — inherits those values, which is why users get `http://` links and a mixed-content or redirect-loop bug report. The missing information exists: the proxy put it in forwarded headers. Something has to consume those headers and restate the request before application code runs. ## RemoteIpValve, and what it rewrites ```xml <Valve className="org.apache.catalina.valves.RemoteIpValve" remoteIpHeader="x-forwarded-for" protocolHeader="x-forwarded-proto" protocolHeaderHttpsValue="https" internalProxies="10\.\d+\.\d+\.\d+"/> ``` Declared inside `<Engine>` or `<Host>` in `server.xml`, the valve rewrites four things on each request: the remote address, the scheme, the `secure` flag, and the server port. `RemoteIpFilter` does the same job as a servlet filter when you cannot edit `server.xml`. Running it as a valve matters for one practical reason: valves execute before the request reaches the web application, so the `AccessLogValve` records the real client address rather than the proxy's — which is usually half the reason you wanted this in the first place. ## The trust walk, which is the real content of the question `X-Forwarded-For` is a *list*: each proxy appends the address it received the connection from, so a request through two proxies arrives as `client, proxy1`. The valve processes that list **from the right**, discarding entries whose value matches `internalProxies` (and recording those that match `trustedProxies` into the `proxiesHeader` output rather than discarding them silently). The first address that is not a trusted proxy becomes the remote address. The reason for walking from the right is straightforward: the rightmost entries are the ones your own infrastructure wrote, and are therefore the only ones you have any grounds to believe. Everything to the left was supplied by someone else and could have been invented by the client. Two configuration mistakes turn this from a fix into a vulnerability: - **`internalProxies` set too broadly.** Widen it to match arbitrary addresses and you are telling Tomcat to believe any hop, which means a client that sends `X-Forwarded-For: 1.2.3.4` gets to choose its own identity. If that address feeds rate limiting, IP allow-listing or audit logs, the control is gone. - **The proxy appending instead of overwriting.** The first proxy at the edge must *replace* any client-supplied `X-Forwarded-For` with the observed peer address, not append to it. Otherwise a client sends a fabricated chain and the trust walk faithfully believes the fabrication once the real hops are stripped. The rule to state in an interview: a forwarded header is only as trustworthy as the hop that wrote it, and the header is data supplied by whoever spoke last. Configure trust explicitly at both ends; never infer it. ## The static alternative on the connector When the topology is fixed and you only care about URL construction, the connector can simply be told what it is behind: ```xml <Connector port="8080" protocol="HTTP/1.1" proxyName="app.example.com" proxyPort="443" scheme="https" secure="true"/> ``` This makes `getScheme()`, `isSecure()`, `getServerName()` and `getServerPort()` report the public values, so redirects and links come out right. It does nothing for the client IP, and it is a lie if that same connector ever receives plaintext traffic directly — which is why a connector configured this way should be reachable only from the proxy. ## Confirming it, and one adjacent trap Verify with a request that carries a deliberately fake header from outside the trusted range and check what the access log records: if your spoofed value appears as the client address, trust is misconfigured. Also confirm the framework layer is not double-handling the same headers — if both a Tomcat valve and an application-level forwarded-header handler are enabled, the second one operates on an already-rewritten request, and the results range from harmless to a confusing address that belongs to neither party. Pick one layer to own the translation and disable the other.
- Why should RemoteIpValve run as a valve rather than as a filter inside the application?Because valves run before the request enters the web application, so everything upstream of the app — most importantly the AccessLogValve — already sees the corrected client address, scheme and port. A filter fixes the request only for application code, leaving container-level logging still recording the proxy's address.
- What breaks if internalProxies is widened to match any address?Client identity becomes attacker-controlled. Any request can carry a fabricated X-Forwarded-For and Tomcat will accept the leftmost value as the remote address, defeating IP-based rate limiting, allow-listing and audit trails. The pattern must cover only the addresses your own proxies actually connect from.
- Tomcat's AJP connector is another way to preserve the original request attributes. What should you check before enabling it?That it is not exposed. Modern Tomcat ships the AJP connector disabled, bound to a loopback address, and requiring a shared secret with `secretRequired` true by default, after a serious vulnerability in exposed AJP ports. Bind it to localhost, set the secret, and only use it when the fronting server genuinely speaks AJP.
saying these in an interview costs you the question
- Trusts X-Forwarded-For without configuring which peers may set it
- Takes the leftmost forwarded entry as the client with no trust walk
- Thinks scheme="https" on the connector also fixes the client IP
- Lets the edge proxy append to a client-supplied forwarded header
- Enables both a Tomcat valve and an app-level handler for the same headers