Configuring Tomcat to See the Real Client IP Behind a Proxy or Load Balancer

When you deploy a Java web application on Apache Tomcat and place it behind a reverse proxy or a load balancer (like Nginx, Apache httpd, or an AWS Elastic Load Balancer), a common challenge arises: Tomcat no longer sees the original client’s IP address. Instead, methods like request.getRemoteAddr() return the IP address of the load balancer itself.

This can be a significant problem for features that rely on the client’s IP, such as:

  • Security and rate-limiting
  • Geolocation-based services
  • Audit logging and analytics
  • User session tracking

Fortunately, Tomcat provides a clean, built-in solution to this problem: the RemoteIpValve. This post will guide you through understanding the problem and implementing the fix in your server.xml file.

The Problem: Lost Client IP Address

Let’s visualize a typical request flow in a load-balanced environment:

Client (IP: 203.0.113.5) → Load Balancer (IP: 192.168.1.10) → Tomcat Server (IP: 10.0.0.50)

When the request reaches your Tomcat application, the direct TCP connection is from the load balancer. Therefore, when your code calls request.getRemoteAddr(), it correctly reports the IP of the last hop: 192.168.1.10.

To solve this, load balancers and proxies add special HTTP headers to the request they forward. The most common header is X-Forwarded-For, which contains the original client’s IP address.

Our goal is to configure Tomcat to automatically read this header and use its value as the “real” remote IP, making it transparent to your application code.

The Solution: Tomcat’s RemoteIpValve

The RemoteIpValve is a Tomcat component that intercepts incoming requests before they reach your web application. It inspects the request headers, extracts the client IP and other proxy-related information, and then modifies the request object to reflect the original client’s details.

The beauty of this approach is that you don’t need to change a single line of your application code. Your servlets, controllers, and filters can continue to use request.getRemoteAddr() and will now get the correct client IP.

How to Configure RemoteIpValve

The configuration is done by adding a element to your conf/server.xml file. This valve should be placed within the or element.

Here is a standard configuration that works for most setups.


<Valve className="org.apache.catalina.valves.RemoteIpValve"
       internalProxies="127\.0\.0\.1|192\.168\.\d{1,3}\.\d{1,3}|10\.\d{1,3}\.\d{1,3}\.\d{1,3}"
       remoteIpHeader="x-forwarded-for"
       protocolHeader="x-forwarded-proto"
       protocolHeaderHttpsValue="https" />

Let’s break down these attributes:

  • className: This must be set to org.apache.catalina.valves.RemoteIpValve to enable the valve.
  • internalProxies: This is a critical security setting. It’s a regular expression that defines the IP addresses of your trusted proxies and load balancers. Tomcat will only trust the forwarding headers (like x-forwarded-for) if the request comes directly from an IP that matches this pattern. This prevents a malicious client from spoofing their IP by sending a fake x-forwarded-for header. The example pattern 127\.0\.0\.1|192\.168\.\d{1,3}\.\d{1,3}|10\.\d{1,3}\.\d{1,3}\.\d{1,3} trusts localhost, and any IP in the 192.168.x.x and 10.x.x.x private network ranges. You must adjust this to match your environment.
  • remoteIpHeader: Specifies the name of the HTTP header that contains the client’s IP. The de-facto standard is x-forwarded-for.
  • protocolHeader: Specifies the header that indicates the original protocol (http or https) used by the client. This is important if you are performing SSL termination at the load balancer. A common header is x-forwarded-proto.
  • protocolHeaderHttpsValue: The value of the protocolHeader that indicates the request was made over HTTPS. Typically, this is https. This ensures that request.isSecure() returns the correct value.

Example: Adding the Valve in server.xml

Open your CATALINA_BASE/conf/server.xml file and locate the element for your application. Add the configuration inside it, like so:


<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">
  ...
  <Service name="Catalina">
    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />
    ...
    <Engine name="Catalina" defaultHost="localhost">
      ...
      <Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">

        <!-- =================================================================== -->
        <!--  REMOTE IP VALVE CONFIGURATION - ADD THIS BLOCK                  -->
        <!-- =================================================================== -->
        <Valve className="org.apache.catalina.valves.RemoteIpValve"
               internalProxies="192\.168\.1\.10|192\.168\.1\.11"
               remoteIpHeader="x-forwarded-for"
               protocolHeader="x-forwarded-proto"
               protocolHeaderHttpsValue="https" />
        <!-- =================================================================== -->

        <!-- Other valves like AccessLogValve can remain -->
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log" suffix=".txt"
               pattern="%h %l %u %t "%r" %s %b" />

      </Host>
    </Engine>
  </Service>
</Server>

In the example above, we’ve configured the valve to trust two specific proxy IPs: 192.168.1.10 and 192.168.1.11. Remember to restart Tomcat for the changes to take effect.

Verifying the Configuration

To confirm that your configuration is working, you can create a simple JSP page in your application to display the request information.

Create a file named ip_test.jsp with the following content:


<%@ page import="java.util.Enumeration" %>
<html>
<head>
    <title>Request IP Details</title>
    <style>
        body { font-family: sans-serif; }
        code { background-color: #f0f0f0; padding: 2px 5px; border-radius: 3px; }
        th { text-align: left; padding-right: 15px;}
    </style>
</head>
<body>
    <h1>Request IP Details</h1>
    <table>
        <tr>
            <th>request.getRemoteAddr():</th>
            <td><strong><code><%= request.getRemoteAddr() %></code></strong></td>
        </tr>
        <tr>
            <th>request.getRemoteHost():</th>
            <td><code><%= request.getRemoteHost() %></code></td>
        </tr>
         <tr>
            <th>request.isSecure():</th>
            <td><code><%= request.isSecure() %></code></td>
        </tr>
    </table>

    <h2>Relevant HTTP Headers</h2>
    <table>
        <%
            String[] relevantHeaders = {"x-forwarded-for", "x-forwarded-proto", "x-forwarded-port"};
            for (String headerName : relevantHeaders) {
        %>
        <tr>
            <th><%= headerName %>:</th>
            <td><code><%= request.getHeader(headerName) == null ? "Not Present" : request.getHeader(headerName) %></code></td>
        </tr>
        <%
            }
        %>
    </table>
</body>
</html>

Deploy this JSP and access it through your load balancer.

Before configuring RemoteIpValve, you would see the load balancer’s IP in request.getRemoteAddr().

After correctly configuring RemoteIpValve, you will see the actual client’s IP address, proving that the valve is working as expected.

Conclusion

Using the RemoteIpValve is the standard, most robust way to handle client IP resolution in a load-balanced Tomcat environment. It’s a “set and forget” configuration that keeps your application code clean and ensures your logging, security, and other IP-dependent features work reliably.

Just remember the most important step: correctly configure the internalProxies attribute to secure your application against IP spoofing attacks.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.