How to set up Apache webserver proxy in front of Apache Tomcat on Red Hat Linux

Objective

Our objective is to setup Apache httpd to work as a proxy in front of the Apache Tomcat application container.

Operating System and Software Versions

  • Operating system: Red Hat Enterprise Linux 7.5
  • Software: Apache httpd, Apache Tomcat

Requirements

Privileged access to the system

Difficulty

EASY

Conventions

  • # – requires given linux commands to be executed with root privileges either directly as a root user or by use of sudo command
  • $ – given linux commands to be executed as a regular non-privileged user

Introduction

Using Apache httpd as a proxy to an Apache Tomcat application container is a common setup. It comes with many use cases, the most trivial is serving static content from httpd, while providing services implementing heavy business logic from an application written in Java that resides in the Tomcat container.

By creating a proxy, we can create a kind of front-end to the application layer, where we can introduce security measures in the webserver, apply load balancing, use conditional redirect, or use any other functionality provided by the webserver. This way we don’t need to implement any of these features in our application, and can focus it’s capabilities to the service itself. We will have a full-featured webserver presented for the users, some of the urls forwarded silently to the application container that may be not accessible by itself. The application’s answers are forwarded back to the clients who will not know that they spoke anything else but the webserver – that is, if we take care not exposing any information (like unhandled error messages) from the application that can make them guess there are more than one layers.

We will use the AJP protocol that can be used between webservers and Java based application containers to provide the ability to balance the load between multiple application servers – however, to set up a load balancer is out of the scope of this tutorial.

We will configure our setup on Red Hat Linux 7.5, but the Apache webserver, the AJP module and the Apache Tomcat application container are available everywhere, and thus this setup is portable with small adjustments like filesystem paths or service names.



Installing required software

First we need to install the services we will use. In a load balanced setup Tomcat server(s) could be on different machines, and often they are, providing a farm of containers that build up a service.

# yum install httpd tomcat tomcat-webapps

We install the tomcat-webapps for testing purposes, within this package is an examples web application deployed into our Tomcat server on installation. We’ll use this application to test that our setup is working as intended.

Now we can enable and start our Tomcat server:

# systemctl enable tomcat
# systemctl start tomcat

And our webserver:

# systemctl enable httpd
# systemctl start httpd

The default httpd installation contains the proxy modules we need. To check that it is so, we can query the webserver with apachectl:

# apachectl -M | grep ajp
 proxy_ajp_module (shared)

Note: 1.x Apache versions use mod_jk module instead of proxy_ajp.

httpd configuration

The examples web application deployed into Tomcat are published after installation by default on server-url:8080/examples. We will proxy requests coming to the server’s port 80 (the default http port) requesting something from the server-url/examples to be served by the examples web application deployed into Tomcat. Requests coming to any other URL on the server will be served by the web server. We’ll set up some static content to show this functionality.

In our example the server is called ws.foobar.com. For the proxy to work create a text file with your favorite editor under the webserver’s drop-in configuration directory, which is /etc/httpd/conf.d on Red Hat flavors, with the extension of .conf. Our setup doesn’t need Tomcat to be reachable directly, so we use localhost as target host in the /etc/httpd/conf.d/example_proxy.conf file:

<VirtualHost ws.foobar.com:80>
  ServerName ws.foobar.com

  ProxyRequests Off
  ProxyPass /examples ajp://localhost:8009/examples
  ProxyPassReverse /examples ajp://localhost:8009/examples
</VirtualHost>

To be on the safe side, we can verify that our configuration is correct before applying with apachectl:

# apachectl configtest
Syntax OK

If the configuration test returns an error like the following:

Could not resolve host name ws.foobar.com -- ignoring!

If means that our ServerName directive is invalid, as it can’t be resolved by the webserver. Either we need to register it in the (local or global) DNS, or provide a line in the /etc/hosts file that contains the host’s public IP address followed by the name we gave in the above configuration. If the hosts file already contains the IP with another name (maybe the real hostname), we can add the servername after the host’s name(s) in the same line, the setup will work.

After successful test we need to apply the new configuration by restarting the webserver:

# systemctl restart httpd


Tomcat configuration

With the default install the Tomcat container will listen to AJP requests on all interfaces on port 8009. This can be verified in the main configuration file:

# view /usr/share/tomcat/conf/server.xml
[..]
<!-- Define an AJP 1.3 Connector on port 8009 -->
    <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
[..]

If we don’t need the Tomcat container and the applications within to be reachable by themselves, we can set every connector to listen only on localhost:

Connector address="127.0.0.1" port=..."

To apply we can restart Tomcat with:

# systemctl restart tomcat

In our lab machine will not do this, as we need to see that we are served the same content on both port 80 and 8080.

Testing

Our minimal AJP proxy setup is complete, we can test it. From the command line we can call the examples application directly on port 8080:

$ wget http://ws.foobar.com:8080/examples
--2018-09-13 11:00:58--  http://ws.foobar.com:8080/examples
Resolving ws.foobar.com (ws.foobar.com)... 10.104.1.165
Connecting to ws.foobar.com (ws.foobar.com)|10.104.1.165|:8080... connected.
HTTP request sent, awaiting response... 302 Found
Location: /examples/ [following]
--2018-09-13 11:00:58--  http://ws.foobar.com:8080/examples/
Reusing existing connection to ws.foobar.com:8080.
HTTP request sent, awaiting response... 200 OK
Length: 1253 (1.2K) [text/html]
Saving to: 'examples'

100%[=========================================================================================================================================================================>] 1,253       --.-K/s   in 0s      

2018-09-13 11:00:58 (102 MB/s) - 'examples' saved [1253/1253]

And see the contents provided:

$ tail examples
<h3>Apache Tomcat Examples</H3>
<p></p>
<ul>
<li><a href="servlets">Servlets examples</a></li>
<li><a href="jsp">JSP Examples</a></li>
<li><a href="websocket/index.xhtml">WebSocket (JSR356) Examples</a></li>
<li><a href="websocket-deprecated">WebSocket Examples using the deprecated
    Apache Tomcat proprietary API</a></li>
</ul>
</body></html>

And if we call the same application trough our AJP proxy, we should also get an answer, while there isn’t any content in the webserver’s document root:

$ wget http://ws.foobar.com/examples
--2018-09-13 11:01:09--  http://ws.foobar.com/examples
Resolving ws.foobar.com (ws.foobar.com)... 10.104.1.165
Connecting to ws.foobar.com (ws.foobar.com)|10.104.1.165|:80... connected.
HTTP request sent, awaiting response... 302 Found
Location: /examples/ [following]
--2018-09-13 11:01:09--  http://ws.foobar.com/examples/
Reusing existing connection to ws.foobar.com:80.
HTTP request sent, awaiting response... 200 OK
Length: 1253 (1.2K) [text/html]
Saving to: 'examples.1'

100%[=========================================================================================================================================================================>] 1,253       --.-K/s   in 0s      

2018-09-13 11:01:09 (101 MB/s) - 'examples.1' saved [1253/1253]

If all works, we will get an answer with the same contents, as the final answer is provided by the same application within the container:

$ tail examples.1
<h3>Apache Tomcat Examples</h3>
[...]

We can also test our setup with a browser. We need to call all URLs with the server’s name as the host (at least the one that is proxied). For that the machine running the browser need to be able to resolve the server name, by means of DNS or hosts file.

In our lab environment we haven’t disabled Tomcat listening on the public interface, so we can see what is provided when asked directly on port 8080:



Tomcat providing the examples application

Tomcat providing the examples application

We can get the same content trough the AJP proxy provided by the webserver on port 80:

httpd providing the examples application with AJP proxy

httpd providing the examples application with AJP proxy

While acting as a proxy, httpd can serve any other content. We can create static content that is reachable on some other URL on the same server:

# mkdir /var/www/html/static_content
# echo "<html><body>Static content</body></html>" > /var/www/html/static_content/static.html

By pointing our browser to this new resource, we are provided with the new static content.

Static content provided by httpd

Static content provided by httpd

If the Tomcat container would not be reachable, we would not know the answer coming somewhere other than the webserver. As we proxied only a specific application, the container’s default ROOT application is not reachable trough the proxy, thus hidden from everything beyond the webserver.

Conclusion

The Apache webserver is highly extendable by the means of modules, one of them is the AJP proxy module. The above guide uses one machine and exposes one application with the proxy, but the same webserver could provide a single entry to many applications, possibly on many hosts running application containers, while providing other web content as well.

Combined with other modules, like mod_security, we can add many features to our service without the need to develop them within the application, or if the need arises, redirect the proxy to another endpoint with a single edition of the configuration file and the reload of the webserver, making a migration or the introduction of the application’s new release a matter of seconds. The same reload can lead the visitor to a page explaining planned downtime, while maintenance is performed on the application servers – the use cases of an AJP proxy are only limited by the imagination of the IT staff.