Apache 2 on OS X: Configuring mod_proxy and SSL
I recently had to setup Apache as a front-end web server for multiple backend servlet containers. The backend containers serve up different web applications, and the Apache front-end unites them from a hostname and port standpoint. The following instructions describe how to configure Apache 2 on Mac OS X to proxy requests to Tomcat or Jetty running on localhost:8080. It also shows how to enable SSL on Apache and force it for certain URLs in your Java web application.
Apache comes pre-installed on OS X, so you should be able to start it by enabling "Web Sharing" in System Preferences > Sharing.
$APACHE_HOME on Leopard is /etc/apache2. On Tiger, it's /etc/httpd. If you've upgraded Tiger to Leopard, it's likely you'll have both directories so make sure you're modifying the right one. I lost a few hours figuring this out, so hopefully this knowledge will appease some googler in the future.
Configuring mod_proxy
- Open $APACHE_HOME/httpd.conf and add the following on line 480 - at the very bottom, just before "Include /private/etc/apache2/other/*.conf".
# # Proxy Server directives. # <IfModule mod_proxy.c> ProxyRequests On ProxyPreserveHost On ProxyStatus On <Location /status> SetHandler server-status Order Deny,Allow Deny from all Allow from 127.0.0.1 </Location> ProxyPass /myapp http://localhost:8080/myapp </IfModule>
ProxyPreserveHost allows request.getServerName() and request.getServerPort() to work as if there is no proxy server in place. In other words, even though Tomcat is running on 8080, request.getServerPort() will return 80.
- The most important line is the last one as this is the dictates the location of your applications. Add more lines as you need to add more applications.
- If everything is configured correctly, you should be able to run sudo apachectl restart and navigate to http://localhost/status. If you receive a "forbidden" error, make sure your /etc/hosts has an entry mapping 127.0.0.1 to localhost (as one of the last entries), or change "Allow from 127.0.0.1" to "Allow from localhost". If you get a "Server not found" error, you can tail the error log at "/var/log/apache2/error_log".
One issue I've seen with mod_proxy is when a request comes in and the backend server is down. When this happens, Apache returns a 503 Service Temporarily Unavailable and it doesn't seem to go away after the backend server is restarted. It does resume proxying after a while, but I haven't determined what causes the proxy to come back to life. If you know a setting that forces mod_proxy to check for the backend server on every request, please let me know.
Configuring SSL
- Open $APACHE_HOME/httpd.conf and uncomment the following on line 470:
Include /private/etc/apache2/extra/httpd-ssl.conf
- Open $APACHE_HOME/extra/httpd-ssl.conf and change line 78 to:
ServerName localhost:443
- In httpd-ssl.conf, change line 99 to:
SSLCertificateFile "/private/etc/apache2/ssl.key/server.crt"
- In httpd-ssl.conf, change line 107 to:
SSLCertificateKeyFile "/private/etc/apache2/ssl.key/server.key"
- In httpd-ssl.conf, add the following after
SSLEngine on
to allow proxying via HTTPS:SSLProxyEngine on
- Follow the Using mod_ssl on Mac OS X tutorial. For "Common Name/Server Name", use "localhost". You can download the source for mod_ssl (which you need at one point during the tutorial) at http://www.modssl.org/source/.
- Run sudo apachectl restart and go to https://localhost. If you get a "Server not found" error, run sudo apachectl -t to verify the syntax of your config files or tail -f /var/log/apache2/error_log to verify there are no errors in the log files.
Forcing HTTPS for certain URLs
If you proxy requests from /myapp -> http://localhost:8080/myapp, request.isSecure()
will return false. If you change it to /myapp -> https://localhost:8443/myapp, request.isSecure()
will return true. I needed to figure out a way to have http://localhost/myapp go to http://localhost:8080/myapp and https://localhost/myapp to go http://localhost:8443/myapp. Even better, I wanted to configure things in a way so request.isSecure()
returned the value based on the originally requested URL, not on the proxied URL. Configuration like the following would be ideal:
ProxyPass http://*/myapp http://*:8080/myapp ProxyPass https://*/myapp https://*:8443/myapp
The solution I came up with is to standardize on secure URLs in my application. That is, use /secure/* as a prefix for all URLs that should be accessed via SSL. To follow this convention and force it, I added the following in my application's web.xml file:
<security-constraint> <web-resource-collection> <web-resource-name>Secure Area</web-resource-name> <url-pattern>/secure/*</url-pattern> </web-resource-collection> <user-data-constraint> <transport-guarantee>CONFIDENTIAL</transport-guarantee> </user-data-constraint> </security-constraint>
Once this is in place, accessing http://localhost/myapp/secure/index.html will result in an error. Accessing it using https will succeed. Following this, you can change your ProxyPass rules to the following and all requests to /secure/* will be https; other requests will be sent to http. The order of the rules below is important.
ProxyPass /myapp/secure https://localhost:8443/myapp/secure ProxyPass /myapp http://localhost:8080/myapp
If this isn't a good strategy for you, Tomcat has the ability to use a redirectPort (in server.xml) that auto-redirects from http to https when CONFIDENTIAL is used in web.xml. I'm not sure if this redirect will carry through values from a form post.