How To Get Drupal To Work Through Strato's SSL Reverse Proxy

DrupalKey

Yesterday, I had two hours in a cafe, a Cappuccino on my table, a piece of lemon cake and free WLAN. "Cool", I thought, "I'll write an entry for my blog!". How romantic.
Right after I entered my blog user's password, it dawned on me: The connection to my blog wasn't encrypted! Anyone able to sniff on the local WLAN would have been able to catch my password as I entered it and steal my blog user ID!
It took me some time (slightly more than the 2 hours I had...) to figure this out, so here's a howto on how to make your login/admin tasks secure for a Drupal instance running on Strato as the hoster.

The Basic Idea

The basic idea behind securing your Drupal admin logins and actions is to make sure all login and admin traffic goes through SSL. That way, passwords, auth cookies, etc. are always encrypted and harder to catch.
This sounds simple in theory, but becomes slightly more complex in practice if you host your Drupal instance in a shared hosting environment like Strato. Here, the way SSL works is by plugging a shared SSL reverse proxy between your web browser and your regular (and unencrypted) Drupal web server. Since both the SSL reverse proxy and your Drupal server sit in the same, trusted network at your hosting provider of choice, this is secure enough for a low-cost solution.

The Problem

The side effect of inserting a reverse proxy between Drupal and your users is that all URLs change: In the case of Strato as a hoster, http://constantin.glez.de becomes https://www.ssl-id.de/constantin.glez.de, with www.ssl-id.de being Strato's SSL reverse proxy and constantin.glez.de being the original hostname, which is now turned into a path component of a reverse proxy manipulated URL. Ugly, but workable. Unfortunately, this doesn't get unnoticed by Drupal, and a couple of problems occur:

  • In the case of multiple Drupal instances that rely on knowing about the host name to choose between them, they'll get confused because the'll get the SSL reverse proxy's hostname in the server's HTTP_HOST variable, not the original host name that the original site was addressed with. In my case, Drupal got www.ssl-id.de as the hostname, not constantin.glez.de out of the web server, and thus it didn't know that the constantin.glez.de Drupal instance was intended. Therefore, Drupal resorted to the default instance, which I didn't want.
  • Drupal generates a handful of relative URLs for images, CSS, Javascript etc. To work correctly, these URLs need to be prefixed with the SSL proxy's hostname and protocol, while the original hostname needs to be worked into the pathnam. But since Drupal doesn't know about sitting behind a reverse proxy, it's generated URLs will be wrong. And without CSS, images, etc., your website tends to look quite ugly.
  • Drupal is prepared to support working behind a reverse proxy, if it knows about it, but what about mixed environments? In our case, we want all login and admin actions to be SSL encrypted (for security) and in all other cases (like for regular blog visitors) to be reached unencrypted (for speed). Now it gets complicated: How should Drupal know when to work in Reverse Proxy Mode and when not?
  • There are some more subtleties associated with being behind a reverse proxy that Drupal needs to take care of: What's the correct cookie host? What's the IP of the reverse proxy, and can we trust it?

The Solution

Fortunately, 80% of the solution was already there, written up in a useful article called "Drupal via HTTPS/SSL Proxy Server (shared certificates)". Check out this solution, because we'll discuss it below, and come back, I'll wait for you.

This is a nice, elegant solution that runs completely inside settings.php, so it doesn't need and module or theme hacking. But after trying it out, I still ran into a couple of issues:

  • Drupal still didn't select the right instance. In my case, the modified settings.php lives in the directory associated with the "constantin.glez.de" instance of Drupal, but the request comes in through "www.ssl-id.de". Since the hostname is different, it will never know that it should go to the "constantin.glez.de" directory to find it configuration.
    Fortunately, the way Drupal searches for its settings.php files allows us to specify not only the host name but also parts of the path components. Hence, the fix here is to create a symlink to "constantin.glez.de" (or any other Drupal instance directory in the sites folder) and name it the way Drupal sees the web request coming in. In this case, I named my symlink www.ssl-id.de.constantin.glez.de and voilá, now Drupal was able to find the right settings.php file, even when called through the SSL reverse proxy.
  • The next issue is probably different for every hosting provider: "Skybow", the author of the original tutorial, used Hosteurope as a hosting provider. Apparently, their SSL reverse proxy sets HTTP_X_FORWARDED_HOST when forwarding requests. Not in the case of Strato. But how can we find out how to detect being behind the SSL proxy? In order to see what exactly happens when you call your Drupal instance, it is helpful to create yourself a file called, say phpinfo.php and put it in your hosting space, then add the following line to it:

    <?php phpinfo(); ?>

    Calling this simple script will let PHP dump its whole environment, so you can debug your code and look for clues in the hoster's server environment.
    Sure enough, phpinfo() confirmed that HTTP_X_FORWARDED_HOST isn't even present in the Strato environment. Instead, I found out that Strato sets SERVER_PORT according to the HTTP security mode ("80" for regular HTTP, "443" for HTTPS). Cool, now I had a new litmus test for Skybow's code and sure enough, changing the first line to:

    $request_type = ($_SERVER['SERVER_PORT'] == '443') ? 'SSL' : 'NONSSL';

    did the trick!

  • The rest was straightforward, but there was still one small change: Apparently, Strato's SSL reverse proxy does make a small effort to fix URL strings, because I did not have to add the original site's hostname to the web server's SERVER_URI variable. So, lines 10 and 10a from Skybow's solution weren't necessary for me.
  • Now, everything sort of worked, but there was still one issue with images that were managed by Drupal's image module. A quick look at the resulting HTML source revealed that their URL was slightly garbled. For example, when SSL-proxied, it created image URLs like https://www.ssl-id.de/constantin.glez.de/sites/www.ssl-id.de.constantin.glez.de/files/sites/constantin.glez.de/files/images/DrupalLightbulb.blog.jpg, which means it inserts one level of /sites...files too many. I'm assuming that to fix this issue, one would need to hack the image module itself, which I don't want to do right now. At this point, I was just lazy and set up another symlink in the sites directory to make the broken URL fit in :).

The Last Step: Fixing .htaccess

Cool, now Drupal works seamlessly, regardless of whether it is called via regular HTTP or HTTPS! Of course, your mileage may vary, especially if you're using a different hoster than Strato with a different proxy. Still, there's a small detail to take care of. How can we force that all user logins and all admin pages get routed through the SSL reverse proxy?
This is a job for the Apache server's .htaccess file. We'll use mod_redirect to force redirection of insecure login/admin accesses to the SSL version of their URLs. Here are the lines that worked for me:

RewriteCond %{SERVER_PORT} ^80$
RewriteCond %{REQUEST_URI} ^/(user|admin)
RewriteRule ^(.*)$ https://www.ssl-id.de/%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

Of course, mod_rewrite has to be enabled for this to work, but if you're using Drupal you're probably using it anyway for some other things. This example used the Strato SSL proxy's host name, make sure you substitute it with whetever different proxy you may be facing.

I hope you found this Drupal hack useful and interesting and that it helped you make your site slightly more secure. Let me know in the comments if you want me to write more about the little hacks that I needed to come up with when putting together this blog. Meanwhile, I'm trying to remember what I wanted to blog about in the first place when I ordered that cappuccino in that cafe yesterday...

Stay in Touch!

Did you like this article? Have you found it useful, interesting or entertaining?

Then click here to get free regular updates and help me reach my goal of 1,000 regular blog readers this summer!

Thank you for reading Constant Thinking.