Restoring Real Client IPs in Tomcat Behind a Proxy with RemoteIpValve
Tomcat behind a load balancer logs the proxy's IP, not the visitor's. RemoteIpValve restores the real client address from X-Forwarded-For and the scheme from X-Forwarded-Proto — if you configure the trust regex and valve ordering correctly.
27 Sept 2025, 01:37 UTC

The logging problem you’ll hit behind a load balancer
Once Tomcat sits behind a load balancer or reverse proxy, request.getRemoteAddr(), the access-log %a pattern, and any IP-based rate limiting or geo logic all report the proxy’s address instead of the visitor’s. This breaks audit trails, skews analytics, and can let abusive traffic slip past IP-based blocks.
Tomcat’s RemoteIpValve (org.apache.catalina.valves.RemoteIpValve) solves this by rewriting the remote address from the X-Forwarded-For header when the direct peer is a trusted internal proxy. It also restores the original scheme via X-Forwarded-Proto so request.isSecure() and generated redirect URLs behave correctly.
How the valve decides which IP to trust
The valve walks the X-Forwarded-For chain from right to left. Every entry that matches the internalProxies regex is treated as a known hop and discarded. The first non-matching IP becomes the client address. If a client forges the header but your proxy appends the real IP, the forged leftmost value is ignored — provided every real intermediary is listed in internalProxies.
Key attributes you’ll set:
remoteIpHeader– header holding the chain (defaultX-Forwarded-For)internalProxies– regex matching your proxy CIDRs (e.g.,10\.0\.0\.0/8|192\.168\.0\.0/16)protocolHeader– header for the original scheme (usuallyX-Forwarded-Proto)protocolHeaderHttpsValue– value that means HTTPS (defaulthttps)
Worked example: nginx → Tomcat on the same host
Assume nginx terminates TLS and forwards to Tomcat on localhost. Place the valve in server.xml inside the <Host> block, before the AccessLogValve so the rewritten IP appears in logs.
<Host name="localhost" appBase="webapps" ...>
<Valve className="org.apache.catalina.valves.RemoteIpValve"
remoteIpHeader="X-Forwarded-For"
internalProxies="127\.0\.0\.1|::1"
protocolHeader="X-Forwarded-Proto"
protocolHeaderHttpsValue="https" />
<Valve className="org.apache.catalina.valves.AccessLogValve"
directory="logs"
prefix="localhost_access_log"
suffix=".txt"
pattern="%a %l %u %t \"%r\" %s %b" />
</Host>
With this ordering, %a logs the visitor’s IP. If you reverse the valves, the access log still shows 127.0.0.1.
Spring Boot shortcut
In a Spring Boot app on Tomcat, you don’t edit server.xml. Instead, set:
server.forward-headers-strategy=NATIVE
server.tomcat.remoteip.remote-ip-header=X-Forwarded-For
server.tomcat.remoteip.internal-proxies=127\.0\.0\.1|::1
server.tomcat.remoteip.protocol-header=X-Forwarded-Proto
server.tomcat.remoteip.protocol-header-https-value=https
NATIVE delegates to Tomcat’s built-in valve, so the same trust model applies.
Trade-offs and limitations
- Scope
internalProxiestightly. A broad subnet (e.g.,10.0.0.0/8) lets any host in that range spoof client IPs. - Header format matters. The valve expects comma-separated IPs (
X-Forwarded-For: client, proxy1, proxy2). It does not parse the RFC 7239Forwardedheader. - CDN → LB → Tomcat chains. Every intermediate hop must match
internalProxies, otherwise the edge CDN IP becomes the “client”. - Version differences. Default
internalProxiesregex and available attributes differ across Tomcat 9, 10, and 11. Check the Configuration Reference for your exact major version. - Missing
protocolHeader. Behind a TLS-terminating proxy without it, Tomcat may emithttp://redirect URLs and non-secure cookies.
Verify it works before you ship
- Run Tomcat behind nginx (or any proxy) that sets
X-Forwarded-ForandX-Forwarded-Proto. curl -H "X-Forwarded-For: 203.0.113.42" -H "X-Forwarded-Proto: https" http://localhost:8080/and confirm the access log shows203.0.113.42.- In a servlet or controller, log
request.getRemoteAddr()andrequest.getScheme()— they should match the forwarded values. - Negative test: hit Tomcat directly (bypassing the proxy) with a forged
X-Forwarded-Forheader. The remote address should stay the direct peer’s IP because it doesn’t matchinternalProxies.
Closing: one valve, one regex, correct logs
Add RemoteIpValve before your access log valve, lock down internalProxies to the exact proxy addresses, and enable the protocol header. That’s the minimal change that restores real client IPs and HTTPS awareness across logging, security filters, and framework helpers. If you can’t touch server.xml, the RemoteIpFilter in web.xml gives equivalent behavior. Test the negative case once — confirming forged headers are rejected — and you’ll avoid the silent misattribution that plagues proxied Tomcat deployments.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.