Session-based Reverse-Proxy

Using mod_proxy is a popular technique to protect web-servers against attacks by suppressing a direct connection to the server running a web-application. With the additional features of mod_security it is possible to build a fine-controllable reverse-proxy which even supports proxying with respect to session-properties. The following article is about reverse-proxying of web-requests using mod_proxy and mod_security.

Problem statement

Using reverse proxies is a common technique to protect application servers from direct access through the internet. The reverse proxy is a single entry to a (probably cluster of) application server and thus hides details about the infrastructure to the internet user.

To achieve this, the proxy module of apache provides the possibilities to map the urls of another server into the namespace of the apache running mod_proxy. This is done by using the ProxyPass and ProxyPassReverse directives of mod_proxy.

Sometimes it becomes useful to redirect a whole http-session to a certain server behind the reverse-proxy (eg. in case one of the application servers failed). mod_security enables you to do proxying of requests with respect to several request-properties and with a small set of extra rules this can be extended to a session-based routing of requests.

Reverse Proxying

The directive ProxyPass and ProxyPassReverse can be used to map all urls with a given prefix on one server into the namespace of the server containing these statements. To set this up for a whole site you can even use / as your url-prefix. Thus the following lines will map the contents of apps.jwall.org.

   #
   # allow proxy-access
   #
   <Proxy *>
       Allow from all
   </Proxy>

   #
   # map all urls below / from the application-server
   #
   ProxyPass / http://apps.jwall.org
   ProxyPassReverse / http://apps.jwall.org

Unfortunately using ProxyPass/ProxyPassReverse gives you little control about when to forward request. Using mod_rewrite you get much more flexible. With mod_rewrite the above scenario will look like this:

   #
   # allow proxy-access
   #
   <Proxy *>
       Allow from all
   </Proxy>

   # 
   # call mod_proxy to forward all requests to the
   # application-server
   #
   RewriteEngine On
   RewriteRule /(.*) http://apps.jwall.org/$1 [P,L]

The first line will enable the rewrite-engine of mod_rewrite. This should be done somewhere in the global config of your server. The next line will forward all requests below / to the application server by giving the request to the mod_proxy-module - this is achieved by the [P]-part of the line. The L at the end of this line ensures that no further rewrite-rules are processed.

Sessioning in mod_security

As the http protocol itself does not provide any session context the applications themselves need to take care of creating and managing a context. This is often integrated in the applications environment (php, j2ee, etc.). The job is to create unique session-identifiers (ID) and make the client send them in each request. This is either done by sending the ID in some request-parameter or in the form of a cookie.

Either way, when working in an apache reverse proxy, mod_security is able to access each part of the request/response messages forwarded between client and server by the proxy module.

By using two simple rules you can create a session-context in mod_security which can be used to store data that is associated with each request belonging to that session. In a way this resembles the applications session context of the user.

Let the application be a PHP application using cookies then the following rules will take care of establishing and following a session:

    #
    # initialize sessioning
    #
    SecRule REQUEST_COOKIES:PHPSESSID !^$ "chain,nolog,pass"
    SecAction setsid:%{REQUEST_COOKIES.PHPSESSID}

The first line ensures that the request contains a cookie, the second one uses this session cookie's value as session-identifier in mod_security. This initializes the collections that mod_security provides for each session. These can be used to hold several attributes, session-score values (eg for blocking whole sessions) etc.

Calling setsid should be done globally and early in the configuration to make the session collections available to all subsequent mod_security rules.

More restrictive Sessioning

To create more restrictive sessions, you can add additional request-properties as for example remote-address or the user-agent to the action that executes setsid. This may result in more precise sessions, but also lead to more sessions than you might want. In experiments I discovered that using the user-agent might not work since for example Safari (Mac OS X) sets user-agent to different values when loading java applets. Trying to use the remote-address makes problems when sessions shall be available beyond a re-addressing in dial-up networks. Most AOL-users are hidden behind several proxy-caches which do a round-robin load-balancing on per-request basis.

Since the session-id is strictly examined limiting the id to the request-cookies seems to be the most reliable way.

Reverse-proxying Based on Session-Properties

When the above rules have been processed, the variables in the collection which mod_security associates with the appopriate request can be used to control the way reverse-proxying is done. For example consider the URL /register.php which is used to process the input coming from a user who typed in data at page /registryForm.html. If the application server-failed (eg. due to an server-error) it will respond with a 5xx-response.

Basically there are two things, that can be done:

  1. Forward the server-error response to the user
  2. Redirect the user to a custom error-page

Most probably you don't want any of these to happen. However using the session-feature of mod_security you could redirect the user back to the input page and proxy subsequent requests of this user's session to a backup server. Imagine we have the important script register.php which processes input that was typed in at registryForm.html. We now use mod_security to mark a session as faulty, if a request to register.do failed (eg returned a response 4xx or 5xx).

   #
   # 
   #
   <Location "/register.php">
       SecRule RESPONSE_CODE ^(4|5) "log,auditlog,phase:3,\
setvar:session.mark=1,redirect:http://server.jwall.org/registryForm.html"
   </Location>

This will mark the session that is associated to this request (set the session-variable mark to 1) and redirect the client back to the input-page registryForm.html.

What we want to happen now, is that all subsequent request belonging to that session get processed by our backup-server which is backup.jwall.org. Since the rewrite-engine cannot access the session-collections of mod_security, we need a little workaround. Setting and reading environment variables is possible for both these modules, so we use mod_securitys setenv-action to make the rewrite-engine know to which server the requests need to be routed.

   #
   # a global rule to reverse-proxy sessions that are 
   # marked (mark == 1) to backup.jwall.org
   #
   SecRule SESSION:mark "@eq 1" "log,phase:1,setenv:server=backup"

   #
   #
   # proxy the request to the server specified in the environment
   # variable  server
   #
   RewriteCond %{ENV:server} ^!$
   RewriteRule (.*) http://backup.jwall.org/$1 [P,L]

   #
   # All the other requests get proxied to our default application-server
   #
   RewriteRule (.*) http://apps.jwall.org/$1 [P,L]

Let's inspect the lines: When the session-collection contains a variable mark with a value of 1, mod_security sets the environment variable server to "backup". Using RewriteCond we do reverse-proxy to the server that is specified in the environment variable server. Thus the first RewriteRule-line is executed only, if this variable is not empty.

The other RewriteRule is a catch-up and functions as default-route. This sends all other requests to the default application-server.

Unfortunately, this solution is not a hot-swapping redirect, as the request causing the error-response is lost, but at least gives the user a chance to enter the input data again.

More Intelligent Approach

A more intelligent approach towards a hot-swapping solution might be to redirect the client using a response-code of type 305. According to rfc-2616 a client receiving this code should repeat the last request once more to the proxy-server specified in the response. When setting the proxy-server to the reverse-proxy, apache should get the request for a second time and redirect it to the backup-server using the global session-mark-rule.

It might be possible to exploit this to get the client re-sent its request. I am currently experimenting if this will work and how reliable it is.