Recovering Real Client IPs in Tomcat with RemoteIpValve
When Tomcat sits behind a reverse proxy, request.getRemoteAddr() returns the proxy IP. RemoteIpValve recovers the real client address—if you configure the trust boundaries correctly.
16 Nov 2025, 05:50 UTC

The proxy blind spot
When you place Tomcat behind a reverse proxy—nginx, HAProxy, or a cloud load balancer—every request arrives from the proxy's IP address. request.getRemoteAddr() returns the proxy, not the browser. Access logs fill with a single upstream address. Rate limiting buckets every user together. Geo-IP lookups point at your own data center. Audit trails lose the origin.
The fix is to make Tomcat read the forwarding headers your proxy already sends. Apache Tomcat's RemoteIpValve does exactly this: it rewrites the request's remote address, scheme, and port before the application sees the request.
How RemoteIpValve rewrites the request
RemoteIpValve (class org.apache.catalina.valves.RemoteIpValve) intercepts incoming requests and inspects headers such as X-Forwarded-For and X-Forwarded-Proto. If the request originates from a recognized proxy, the valve replaces the remote address with the client address recorded in X-Forwarded-For, updates the scheme to match X-Forwarded-Proto, and adjusts the port if configured. The servlet container then passes the modified request to your application.
The valve does not trust every caller. It distinguishes two categories: internalProxies (a regular expression matching private network ranges by default) and trustedProxies (an explicit list of upstream addresses). Only requests arriving from these ranges are rewritten. A direct request from the internet, even one carrying forged X-Forwarded-For headers, is left untouched.
Note that while Tomcat 10 and later use the jakarta.servlet namespace for application code, RemoteIpValve remains in org.apache.catalina.valves, and the XML configuration syntax is stable across Tomcat 8.5 through current releases.
A worked context.xml configuration
A typical configuration lives in context.xml or a context fragment deployed with your application. The following example enables the valve with common defaults:
<Valve className="org.apache.catalina.valves.RemoteIpValve"
remoteIpHeader="X-Forwarded-For"
protocolHeader="X-Forwarded-Proto" />
Place this inside the <Context> element. If you use a per-application context fragment, the file typically lives in $CATALINA_BASE/conf/[enginename]/[hostname]/ and must be readable by the user running Tomcat. After adding the valve, restart the affected Tomcat host or instance. A restart is required because valves are instantiated during context startup.
Trust boundaries: what internalProxies and trustedProxies actually gate
The default internalProxies regex covers common private ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, and 127.0.0.1). If your proxy runs in a container network, a custom VPN, or a non-standard subnet, the default may miss it. In that case, requests from your proxy will not be rewritten, and the application will continue to see the proxy address.
Conversely, if internalProxies is too broad, an attacker who can reach Tomcat directly could spoof X-Forwarded-For and mask their real address. The trustedProxies attribute lets you lock this down further by listing only the specific upstream IPs or regex you control.
A common mistake is assuming the valve validates header content. It does not. It only checks the immediate caller's address. If your proxy chain has multiple hops, X-Forwarded-For may contain a comma-separated list; the valve processes the chain and selects the client address after removing recognized proxy hops. You need to know your network path.
Verifying the rewrite end-to-end
Do not assume the valve works because the XML is syntactically correct. Deploy a simple diagnostic endpoint that returns the values your application would see:
// Example servlet or controller snippet
String remoteAddr = request.getRemoteAddr();
String scheme = request.getScheme();
int serverPort = request.getServerPort();
// Return these as plain text or JSON
From a host whose address matches your internalProxies regex (or through your actual proxy), send a request. If you have shell access to an intermediate host, you can simulate a proxy hop with curl:
curl -H "X-Forwarded-For: 203.0.113.10" -H "X-Forwarded-Proto: https" http://tomcat-host:8080/diagnostic
Expected check: the response should show 203.0.113.10 as the remote address and https as the scheme. If it shows the proxy IP instead, the immediate caller is not matching internalProxies or trustedProxies.
Next, inspect the Tomcat access log. The default pattern logs %h for remote host and %{X-Forwarded-Proto}i if configured. After the valve is active, %h should reflect the rewritten client address, not the proxy. Coordinate with monitoring teams, because historical dashboards and alert thresholds that keyed on the proxy address will shift.
Limitations and rollback
Enabling RemoteIpValve is a configuration change that alters request state. If the proxy sends X-Forwarded-Proto: https but your application issues redirects, those redirects may change from http:// to https://. Cookie Secure flags may also behave differently. Test HTTPS termination end-to-end.
If the rewrite causes unexpected behavior, rollback is straightforward: remove or comment out the <Valve> element in context.xml and restart the Tomcat host. The request will revert to the proxy's address.
Finally, verify against your specific Tomcat version. The valve has existed since early Tomcat releases and the configuration syntax is stable across Tomcat 8.5 through current versions, but default regex values and attribute names can evolve. Check the documentation for your deployed version before relying on defaults in production.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.