Drupal Mixed-Mode SSL Behind an Nginx Reverse Proxy

We were having a situation with a site where sessions weren’t being shared between insecure (http) and secure (https / SSL). The one major difference with this site was that we were using Nginx as a reverse proxy, but we weren’t quite sure how it was affecting us. We ultimately found the cause to be a combination of Nginx settings and how Drupal differentiates between different domains.

(HINT: If you want the short version, and are having similar issues, try setting cookie_domain in settings.php!)

Mixed-Mode SSL

Everyone knows that SSL is a required technology when passing “sensitive” data over the Internet. That is, anything that, if it got into the wrong hands, could cause someone some significant harm (Credit cards, social security numbers, passwords etc.). On a particular client site, we are selling roles (subscriptions to premium content) and need SSL since we’re taking credit cards on-site to provide a seamless user experience. First, let’s talk about why there are so many moving parts here. (I should also mention that a lot of this applies to Drupal 7, but our case study is based on a Drupal 6 site.) Generally, there’s four ways you can handle SSL (from this Drupal Scout article):

  • Use a module like securepages to force SSL on certain pages. This is all well and good, but since the session cookie can eventually be passed in plaintext later, it doesn’t do much in the way of actual security because the session can be hijacked and someone can gain access to the secure portions of your site.
  • Add hijack prevention to securepages. This additional module, logs a user out if it detects someone trying to hijack a session by keeping two cookies (a secure and an insecure) and ensures they match the incoming SSL request; If not, it logs the user out. This doesn’t necessarily prevent the hijack itself, but it increases security when it detects a hijacked session.
  • Once someone is logged in, serve their entire session over SSL. (The downside is that if someone has the home page bookmarked, for example, they might appear to be logged out though they are logged in on the https side.)
  • Only use SSL for the site. (Don’t even respond on port 80 with a redirect).

We chose model #2 because a history of bad user experience (users becoming/appearing logged out) ruled out #3. Serving SSL-encrypted pages does come with some performance overhead, so generally the practice is to run SSL in “mixed-mode,” meaning to secure only the pages that transmit the sensitive information.

Nginx Reverse Proxy

Drupal and Apache are not the most performant pieces of software, especially as modules are added to the fray. We were originally brought on to improve this sites performance. We decided that using Nginx as a reverse proxy (directly serving static files like css, js, and images) and passing only non-trivial requests back to Apache was the magic bullet. Apache runs on a non-standard port so the two don’t conflict. Here’s a snippet of the Nginx config, to give you an idea: … location / {     ...     root /var/www/oursite.com;     ...     proxy_pass http://www.oursite.com:8080;   } There’s some obvious stuff missing (e.g., a regex to match images and serve them directly, etc.), but that crucial proxy_pass directive will be a major player here soon. The following diagram should help you visualize the setup:

The Issue

Like I said, once we installed e-commerce and got SSL running with securepages (and securepages_prevent_hijack), we noticed that users were appearing logged out when switching from http to https. To that end, we decided to redirect all traffic to https so that users wouldn’t log out. Unfortunately, our client started to see a decline in traffic, particularly from organic search results (Google) following the switch to https. We checked for a few things, chief among them that the analytics themselves were correct. After an analysis there, we concluded that they were -- and that the traffic decline was real.

The Solution

We referred our client to the Drupal SEO professionals at Volacci to help them develop an SEO strategy and implement some best practices. Among the recommendations was that the site go back to mixed-mode SSL. For that to be successful, we needed to get to the bottom of the “logout situation” (that sessions were not being shared between SSL and non-SSL variants). My motto is always to “go to the code.” If you want to know why things are behaving the way they are, you gotta go to the code! (And potentially dump some variables as you go!) The problem is here: if ($cookie_domain) {     // If the user specifies the cookie domain, also use it for session name.     $session_name = $cookie_domain; } else {   // Otherwise use $base_url as session name, without the protocol   // to use the same session identifiers across http and https.   list( , $session_name) = explode('://', $base_url, 2);   … } As you can see, the idea is that it uses a cookie domain, if set (in settings.php) to key the session. If you don’t set a cookie domain, it uses the HTTP_HOST (stripping the PROTOCOL so that sessions will be shared, but NOT stripping the port number). The port number is passed as part of the HTTP_HOST (remember that proxy_pass directive from the nginx config?). In our config, we have Apache listening on port 443 and handling all the SSL traffic directly since it is minimal. Over SSL, the HTTP_HOST value is “oursite.com” but when proxied back the HTTP_HOST it is “oursite.com:8080” - which matches the proxy_pass directive from above. This causes two different sessions to be created in Drupal’s sessions table and treats http and https as two separate areas. To fix, well, you can see it in the code! By specifying our cookie_domain in settings.php, we were able to keep all the sessions keyed the same, and switching back and forth from http and https maintained the session state. Hypothetically, another solution would be to let nginx handle the SSL and to proxy_pass the SSL requests over the localhost also on :8080, and the HTTP_HOSTs would match, but I prefer the idea all the SSL is singly handled directly by Apache. Has anyone else encountered this? Does anyone have alternate solutions or problems with SSL and a reverse proxy config? We’d love to hear about your situations.

Hey Chris- Great overview of

Hey Chris- Great overview of the client situation and thanks for articulating the work our team did to help the client. Appreciate the mention!

The overview was really

The overview was really formulated well and in advanced

Mixed mode is bad. As you

Mixed mode is bad. As you cannot mark the cookies as secure. Also Nginx SSL performance dwarfs Apache. It's usually used as a SSL terminator for sites having Apache handling PHP processing.

Your solution seems overcomplicated at first sight.

perusio

How you have described it is

How you have described it is exactly how we HAD it set up initially, but mixed mode was recommended to us for SEO purposes.

While we would like to run the SSL piece also through nginx, this solution was faster for us to get running (just through more familiarity with Apache SSL) and the performance increase wasn't making or breaking our site and won't for quite some time.

This is just the particular set of circumstances we had to contend with at this time.

I had a very similar issue

I had a very similar issue with drupal. It fails to load on HTTPS with Google Chrome (works fine nonetheless with Firefox). Finally I've seen that Chrome works "ok" when directing requests to Apache HTTPS but fails when using nginx proxy to Apache HTTP. I ended up with a quick to implement solution: configure nginx to proxy HTTPS to Apache HTTPS. It is ugly, not performance wise, but a simple nginx reload showed me it works OK.

On your implemented solution is looks ok. But I'd prefer to bind a session cookie to HTTP-SSL only instead of the entire domain. This is to avoid using the cookie on HTTP not encrypted requests. This sharing issue lowers the security of the entire site. A dual cookie could be interesting: to identify the user and to identify the session (separately).

Yours,
jordi.ferran@gmail.com