Mixing HTTP and HTTPS access to an application

By Abhijit Menon-Sen <ams@toroid.org>

2011-02-10

I'm working on a web application that is running behind a reverse proxy. Most people will use HTTP to access it, but anyone who wants to login must use HTTPS until they logout, to avoid leaking plaintext passwords and session cookies. This is a brief note about the configuration. I'm using Mojolicious and Apache 2.2's mod_proxy, but the implications of providing mixed HTTP/HTTPS access through a reverse proxy are relevant to other implementations.

The application accepts HTTP requests on localhost:3000, and two Apache VirtualHosts named example.org are configured to forward HTTPS and HTTP requests to it as follows.

Listen a.b.c.d:443
<VirtualHost a.b.c.d:443>
    ServerName example.org
    # …SSL configuration…
    ProxyRequests off
    ProxyPass / http://localhost:3000/
    ProxyPassReverse / http://localhost:3000/
    RequestHeader set X-Forwarded-Protocol "https"
</VirtualHost>

<VirtualHost a.b.c.d:80>
    ServerName example.org
    ProxyRequests off
    ProxyPass / http://localhost:3000/
    ProxyPassReverse / http://localhost:3000/
    RequestHeader set X-Forwarded-Protocol "http"
</VirtualHost>

The ProxyPass directives set up reverse proxies for requests from https://example.org and http://example.org to http://localhost:3000 (mod_proxy always talks to the application using HTTP, no matter how the client talks to mod_proxy). The ProxyPassReverse directives ensure that http://localhost:3000 URLs in Location headers from the application are rewritten to http://example.org URLs before they are sent to the client. Finally, we set X-Forwarded-Protocol depending on whether the client is using https or http (i.e., depending on which VirtualHost we receive the request in). In the application, we can ask if the request was made over a secure channel by checking if the header is set to https.

All of this works. The difficult questions arise when the application knows a request was plaintext and wants to issue a redirect to the same URL through the secure server. Suppose the application sends:

Location: https://localhost:3000/foo

Our ProxyPassReverse directive doesn't match this URL, so it reaches the client as-is, without being rewritten. There are three ways we could try to fix this. First, adding "ProxyPassReverse / https://localhost…" would rewrite the URL to "https://example.org/foo". Second, mod_proxy sets the X-Forwarded-Host header to the value of the Host request header (but we could "RequestHeader add" it by hand too), and we could use it to issue a redirect to "https://example.org/foo". Or we could teach the app its canonical HTTP and HTTPS URLs and use that to construct redirects.

Adding a new ProxyPassReverse line is the easiest solution, but it does mean that the application would (at various times) be issuing redirects to both "http://localhost:3000/…" and "https://localhost:3000/…", thus claiming in some sense to accept both HTTP and HTTPS requests on port 3000. Of course, this is just a workaround for the proxy's limitations. We are forced to split the service across two independent VirtualHosts, and there's no better way to switch protocols (like the hypothetical "X-Proxy-Force-SSL" response header suggested by my friend rjbs).

Another thing to note is that both the second ProxyPassReverse line and the X-Forwarded-Host solution will work only if the two servers are both running on standard ports (80 and 443), so that "http://example.org" and "https://example.org" both work. If that is not the case, the right URLs will have to be hardcoded into the app somehow.

I ended up adding new ProxyPassReverse lines.