Redirecting IE9 compatibly with nginx when behind a load balancer

Events happening in the community are now at Drupal community events on www.drupal.org.
Torenware's picture

I'm having a nightmarish problem getting IE 9 to redirect from http to https. I've looked at a discussion from about a year ago (http://groups.drupal.org/node/206813), but in that case, nginx is in complete control.

In my case, I'm in AWS, and https is actually getting handled by an elastic load balancer (ELB), which is proxying over to nginx over port 80, and setting http_x_forwarded_proto:

        #x_forwarded_proto stuff for elb/https issues - see http://daniel.hahler.de/handle-x-forwarded-proto-in-backend-nginx
        set $my_https "off";
        if ($http_x_forwarded_proto = "https") {
          set $my_https "on";
        }

        # http to https rewrite
        if ($http_x_forwarded_proto = "http") {
           rewrite ^(.*) https:/$host$1 redirect;
        }

This works fine on a well engineered, stable browser. Unfortunately, IE 9 is not one of those. If security is set to typical levels of paranoia by the end user, IE gives a cryptic "Internet Explorer cannot display the web page" error with a helpfully unhelpful "Diagnose Connection Problem" that craps out and says, in so many words "you're on your own brother".

I suspect that some of you have seen this. Is there a better way to do this redirect so that IE 9 will, well, behave.

Using Fiddler 2, I see that our server (going back from nginx via the ELB) returns this:

HTTP/1.1 302 Moved Temporarily
Content-Type: text/html
Date: Thu, 16 May 2013 01:11:08 GMT
Location: https:/preview.our-site-name.org/
Server: nginx/1.2.6
Content-Length: 160
Connection: keep-alive

IE responds this with its "Cannot display web site" error, w/o any further explanation.

Is there an nginx specific set up that will fix this? Or is there something we need to do on the ELB?

Also, anybody that can tell me how to get IE9 to give useful (or any) diagnostics will gain much karma, and the esteem of many of his or her peers.

Comments

schema

mikeytown2's picture

I see https:/preview.our-site-name.org/ shouldn't this be https://preview.our-site-name.org/ as it it needs 2 "/"

Also, and I'm sure @perusio

bhosmer's picture

Also, and I'm sure @perusio will weigh in with some more experience, what if you did your redirect like this: http://serverfault.com/questions/67316/in-nginx-how-can-i-rewrite-all-ht... instead of using all of the if statements?

Yep

perusio's picture

you have a missing / could be that. Also that blog post makes reference to a very old version. There's no need to explicitly set the HTTPS FCGI param.

You should use a server block like this:

server {
    listen 80;
    return 301 https://$host$request_uri;
}

Thanks for jogging my memory.

bhosmer's picture

Thanks for jogging my memory. I like that one!

Where does this server block sit in the config

Torenware's picture

perusio --

I'm using a CentOS 6 deployment which uses a Debian style "sites-available" type of directory.

Right now, the file for this particular server has a single server block. I'm a bit unclear where the redirecting server block you recommend above sites in the file. Should it be first, before the server block with the rest of the settings you detail below?

Instead of a redirect, how

brianmercer's picture

Instead of a redirect, how about something in your drupal settings.php like:

if (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) {
  $base_url = 'https://preview.our-site-name.org';
}

At the end of this issue are

brianmercer's picture

At the end of this issue are some other ways of doing that:
http://drupal.org/node/313145

This one looks nice:

if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO'])) {
  $_SERVER['HTTPS'] = 'on';
}

Another idea:  fastcgi_param

brianmercer's picture

edited...that should probably be

map $http_x_forwarded_proto $php_https {
  default '';
  https on;
}
...
fastcgi_param HTTPS $php_https;

Much thanks

Torenware's picture

I'm going to try out these suggestions tomorrow, and see if they resolve the IE 9 related problem.

Even if not, these should be a cleaner and easier maintained version of the code I'm using now.

So continuing

perusio's picture

Nginx introduced in version 1.1.11 a flag for variables if_not_empty. So that the FCGI config has:

fastcgi_param HTTP $https if_not_empty;

That should be your first shot. If that doesn't work this will fix it:

map $scheme$http_x_forwarded_proto $https {
    httpshttp on;
    httpshttps on;
    httpshttp on;
    httphttp on;
}

And you use the same line:
fastcgi_param HTTP $https if_not_empty;

For the redirect from HTTP to HTTPS use the server block I've written above.

Where does the server block go in the configuration

Torenware's picture

Perusio --

If I'm in a mulit-site set up, where does this server block go?

This looks to me like an non-conditional redirect. Since I am getting proxied over at port 80 from the ELB for both http and https URLs, would I not want to do that redirect ONLY in the http case? It looks like what you are suggesting would cause a redirect loop if I used it naively (which sadly, is the only way I'm using this stuff right now).

I know that multiple server blocks are greatly preferred to using "if", but I'm not clear how to get what looks like conditional behavior here without it.

Yeah. Look at this issue:

brianmercer's picture

Yeah. Look at this issue: https://forums.aws.amazon.com/message.jspa?messageID=405375

You need to configure ELB so that it sends http -> http and https -> https.

The redirect is inefficient,

brianmercer's picture

The redirect is inefficient, unnecessary and I would guess that the switch from 302 to 301 still won't satisfy ie9.

The front end proxy (ELB in

brianmercer's picture

The front end proxy (ELB in this case) is tasked with handling ssl negotiation and encryption with the client. Communication between the proxy and the back end server should not be encrypted (unless it's over a public network, in which case ELB should not accessing the backend nginx server on port 80).

Take the static assets for example: Someone requests a css file over port 80 and the proxy requests the file from nginx over port 80 and all is well. Now If someone requests a css file over ssl, port 443, then the proxy requests the file from nginx over port 80, unencrypted, nginx returns it unencrypted, and then the proxy encrypts the file and sends it back over ssl. Using the redirect discussed above will cause nginx to rewrite the request, process it again, encrypt it, and send it back to the proxy over ssl. All for no reason and at the cost of some processing power. And it will waste that processing power even on client requests that were not over ssl.

The problem is with the dynamic requests to Drupal. Without any special configuration, an incoming request over ssl will reach the proxy which will make a request to nginx on port 80 and nginx will tell Drupal that the request is not ssl. Drupal will return a page and nginx will send it back to the proxy and the proxy will encrypt it and return it to the client over ssl just fine...however, all the link and script tags and some other stuff embedded in the Drupal page will point to http:// and not https://, since Drupal didn't know any better. At that point, a browser will recognize that the page itself is over ssl but some of the links contained in it are not in ssl. Since the browser doesn't know which are important it defaults to saying that the "mixed" mode of ssl and non-ssl is insecure. Some browsers might put a red line thru the lock symbol and some with stricter security might lock you out entirely.

The solution is to tell Drupal that the page that it is going to return should be written for ssl. Depending on the version of Drupal you're using the bootstrap.inc is going to contain something like:

// Create base URL
$base_root = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https' : 'http';

and then use $base_root to create $base_url.

So you can use the special HTTP_X_FORWARDED_PROTO header to inform Drupal of the ssl status using php at the Drupal stage in your settings.php with something like

if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO'])) {
  $_SERVER['HTTPS'] = 'on';
}

(assuming that ELB sends that header only for https, otherwise you'd have to do a more complex test). Then Drupal will know to send back a page with https links.

Or you're probably better off doing it at the nginx stage with:

  map $http_x_forwarded_proto $php_https {
    default '';
    https on;
  }

  fastcgi_param HTTPS $php_https if_not_empty;

(Good call on the if_not_empty but you can't use $https as the variable since it's a built-in variable now). The map directive should be at the http level and the fastcgi_param is at the location level, though you can probably just put the line in your fastcgi_params file if Centos is anything like Debian.

The redirect might work most of the time since nginx is telling Drupal that the request is over ssl on every single request, but it's using resources unnecessarily and then on regular requests NOT over ssl, the embedded links will still be https! So every single static file will be requested over ssl, whether the main page is over ssl or not.

i need to redirect some subset of paths to https

Torenware's picture

Brian,

I'm looking at your suggestions, and trying them out right now. If I go to https://my.site.org, it displays fine. And if I go to http://my.site.org, it displays correctly over HTTP, but does not redirect to https://my.site.org, which is the desired behavior.

I've added

<?php
if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO'])) {
 
$_SERVER['HTTPS'] = 'on';
}
?>

to settings.php, added
map $http_x_forwarded_proto $php_https {
    default '';
    https on;
  }

to my nginx.conf file, which is the top-level config file, containing the http {} block, and which calls my file from sites-enabled/ that contains the server{} block below the point I added your map directive.

In mysite.conf, which is called via that include, I have a location block like this; I've added the xx at the end of it, as so:

        location ~ \.php$ {
                fastcgi_split_path_info ^(.+\.php)(/.+)$;
                #NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini
                include fastcgi_params;
                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
                fastcgi_intercept_errors on;
                fastcgi_pass unix:/tmp/phpfpm.sock;
                #fastcgi_param  HTTPS    $my_https;
                fastcgi_param HTTPS $php_https if_not_empty;
        }

What am I missing here to make http://my.site.org redirect to https://my.site.org behind an ELB (which is configured as you describe)?

I've started reading Dimitri Aivaliotis' book to help me better understand how configuring nginx works; I'm an apache guy, so I'm still doing this from a low level of comprehension :-)

It ain't necessarily so

perusio's picture

(Good call on the if_not_empty but you can't use $https as the variable since it's a built-in variable now)

In fact you're right. The https variable has a special behaviour. You cannot override it. But it's not the common case. You can overwrite most variables, e.g., $scheme. In that respect Nginx handles you the keys and it's your responsability to behave properly.

It's the only variable in ngx_http_variables.c that is set to the empty string if the scheme is not https.

static ngx_int_t
ngx_http_variable_https(ngx_http_request_t *r,
    ngx_http_variable_value_t *v, uintptr_t data)
{
#if (NGX_HTTP_SSL)

    if (r->connection->ssl) {
        v->len = sizeof("on") - 1;
        v->valid = 1;
        v->no_cacheable = 0;
        v->not_found = 0;
        v->data = (u_char *) "on";

        return NGX_OK;
    }

#endif

    *v = ngx_http_variable_null_value; // here it is

    return NGX_OK;
}

I suppose that is necessary to make it work with the if_not_empty flag.

Well then perusio is right on

brianmercer's picture

Well then perusio is right on at http://groups.drupal.org/node/298833#comment-925198

Ideally the front end proxy should be able to do this, but a quick Google search tells me that ELB cannot handle the redirect and you have to do it at the back end.

You need two server sections, one short one like perusio stated:

server {
    listen 80;
    return 301 https://$host$request_uri;
}

and a second one configured for ssl with all of the configuration in it:
server {
    listen 443 ssl;
    ssl_certificate ...
    ssl_certificate_key ...

    [All configuration here]
}

ELB is handling SSL

Torenware's picture

Brian,

Am I understanding you that in general, we cannot have the ELB handle SSL? If so, why is this?

We can certainly move SSL handling from the ELB to nginx (on our instances). I just want to understand why this is.

No, you can and should have

brianmercer's picture

No, you can and should have ELB handle SSL.

What I'm saying is that since ELB can't do the redirect, you'll have to set up ssl on both ELB and nginx.

And yes, that's wasteful, but necessary due to the limitation of ELB.

The guy in this issue is

brianmercer's picture

The guy in this issue is right though: https://forums.aws.amazon.com/message.jspa?messageID=405375

Your front end proxy SHOULD handle this and save the backend the work of encrypting. However, that answer from an AWS tech from 6 months ago says that it is not a feature of ELB.

OK, so I gave it some more

brianmercer's picture

OK, so I gave it some more thought and I spun up an EC2 instance and load balancer and I think there's a better way to do it.

I set the load balancer with two listeners:

http/80 to http/80
https/443 to http/8080

(remember to add 8080 to the security group of the ec2 instance)

The nginx configuration will still have two server blocks. The 80 server block will be the same as above and will redirect to https.

server {
    listen 80;
    return 301 https://$host$request_uri;
}

The second server block will accept http with no ssl keys or anything and it will have all the configuration in it:
server {
    listen 8080;

    [All configuration here]
}

The second block will have all the usual configuration BUT you won't need the map or the settings.php stuff. All you will need to use is

  fastcgi_params HTTPS on;

And that will tell Drupal to treat anything coming in that way to go out with https:// links.

Since you have a second server block for all ssl traffic, you don't need to read the HTTP_X_FORWARDED_PROTO header at all.

And this way the back end server doesn't have to deal with ssl encryption.

Have experieced this

jamonation's picture

I've dealt with IE8/9 and upgrading to HTTPS using a front-end proxy and rewrite rules. The trick I found was that IE was actually trying to negotiate HTTPS over an HTTP connection and was failing.

The error was:

400 Bad Request
The plain HTTP request was sent to HTTPS port
nginx

Putting this in the front-end nginx was step one:

server {
        listen 80;
        ssl on;
        rewrite ^(.*) https://$host$1 permanent;
        error_page 497 https://$host$request_uri;
}

I'm pretty certain it was the error_page 497 that was showing up in the logs that led me to find this common solution to the first error. Firefox and Chrome didn't have any problems. My setup is also using HSTS to force SSL for all visitors, but that only handles returning traffic.

In this instance this fix would handle SSL instead of ELB as dsicussed.

Next up is the proxy_set_header directive (normal):

        proxy_set_header X-Forwarded-Proto $scheme;

Then in my front-end proxy and backend Drupal fastcgi_params (normal):

        fastcgi_param   HTTPS   on;

Hope this helps a bit, even in an ageing thread, or that at least anyone searching for this issue stumbles upon this thread.

Nginx

Group organizers

Group notifications

This group offers an RSS feed. Or subscribe to these personalized, sitewide feeds: