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.