Thursday, October 25, 2012

War Story: Apache, SSL, and name-based vhosts

Note: this post was written about a year ago, before we completed some major upgrades to our infrastructure.  I meant to post it as soon as we were done, but it got buried under too many other posts and drafts.  The original post follows, without edits for temporal accuracy.

You can do it The Right Way and use SNI if:
  1. You don't care about Internet Explorer (7 and 8) on Windows XP.
  2. You have Apache 2.2.12 or newer.
  3. You have openssl 0.9.8f or newer with TLS extensions; extensions are included by default in 1.0.
If all of these are okay for you, then setup is a matter of enabling NameVirtualHost *:443 and setting up TLS vhosts in much the same way as regular vhosts.

Otherwise, you have to try a bit harder.

Windows XP is not something we can avoid yet (nobody wants to know "Besides the install fee, you need to upgrade your computers" when that's not a requirement for our competitors' product) so I needed a way for non-SNI browsers to access the non-default TLS vhosts.

After a bit of searching and thinking, I decided on a solution: a certificate making use of alternative names, that I could use on every vhost and validate every vhost name.  Thus, when is accessed and ends up in Apache's vhost, the certificate sent says "Hi, I'm and!"  Then the browser accepts the certificate as matching and the secure session gets set up properly.

But there's a problem: Apache routed the request to so now the request will come in, looking for one of foo's resources inside std's namespace.

I grabbed the mod_rewrite sledgehammer and applied it.  In the VirtualHost block corresponding to the default SSL vhost:
RewriteEngine on
RewriteCond %{HTTP_HOST} ^(?:www\.)?foo\.example\.com
RewriteRule .*$0 [P,L]
There is no ProxyPassReverse directive because any URL that comes back from the proxy still points to the same server this default vhost is on.  If the original request ended up on this vhost, then any direct reference to the proxy will also end up on this vhost again.

I believe this retains the safety of TLS in my specific scenario because the proxied URL points back to the same interface, which I can guarantee through /etc/hosts.  Like ProxyPassReverse, if the traffic ended up on the server, then it's the server for both sites.  However, your scenario may differ.

One potential disadvantage here is the inability for the application to tell whether it's secure by checking for HTTPS being "on" anymore.  I solved it by checking for X-Forwarded-For, only if the remote client is the server itself (well, technically on the LAN, but if I can't trust myself....).  This prevents the web users from stuffing in X-Forwarded-For and causing the server to assume HTTPS mode on a direct HTTP connection. In the configuration for the receiving virtual host:
RewriteCond %{REMOTE_ADDR} ^192\.168\.42\.
RewriteCond %{HTTP:X-Forwarded-For} . [NV]
RewriteRule ^/ - [E=PROXY_HTTPS:on]
The X-Forwarded-For line exists only to set the "no vary" flag, NV: my mod_rewrite proxying is invisible to the user, and X-Forwarded-For should be non-varying coming from the REMOTE_ADDR, so it's safe to block it from appearing in Vary.  Without NV, the Vary header would leak the knowledge that proxying was in use on the backend, simply because X-Forwarded-For would appear there.

Another potential disadvantage is for a lot of non-SNI connections to fill up the TCP port list: each connection from the default vhost into the target will require a unique source port, since the source IP, destination IP, and destination port will be the same for each connection.

I'm also adding a bit of memory usage and latency to servicing the requests.  But I don't think that the traffic we have is going to cause problems, and if we do get too much traffic, then we can justify moving to a multi-server setup that can handle having separate public IPs and this mess all disappears.

(This did end up working out for the traffic we had.  YMMV.)

No comments: