Sean Carpenter

Rails SSL Route Generation With Nginx and Unicorn

I recently enabled SSL for a Rails 3 site that I’m working on. I thought it would be simple since the entire site will be served over SSL (no switching between SSL and non-SSL requests based on the page).

Since the entire site will be served over SSL, I configured nginx to do the SSL redirects for me:

nginx.conf
1
2
3
4
5
server {
  listen      80;
  server_name localhost test.something.com;
  return 301 https://test.something.com$request_uri;
}

Now whenever a non-SSL request comes in, nginx will redirect to the same path using SSL. My SSL server configuration is:

nginx.conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
server {
  listen      443;
  ssl         on;
  ssl_certificate     /path/to/crt.crt;
  ssl_certificate_key /path/to/private.key;

  root        /var/www/something/current/public;
  index       index.html index.htm;

  server_name localhost test.something.com;

  try_files   $uri/index.html $uri.html $uri @app;

  location @app {
      proxy_set_header    X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header    Host $http_host;
      proxy_redirect      off;
      proxy_pass          http://unicorn_something_server; # proxy to a Unicorn upstream configured earlier in the file
  }
}

This is just the same configuration I previously had (before adding SSL) with the listen directive changed to 443 and the ssl directives added.

After making these changes and restarting nginx, everything seemed to be working correctly. Any requests to the non-SSL site were redirected with a 301 response code as expected.

After a day or so I noticed that URLs being generated by Rails were not using the https scheme. I originally noticed it with named routes like so:

1
2
3
4
5
6
7
8
9
10
# In routes.rb
get "dashboard" => "dashboard#index", as: "dashboard"

# In a controller
def create
  # create something
  redirect_to dashboard_url, alert: "Creation successful"
end

# The generated route from redirect_to would be "http://test.something.com/dashboard"

Anywhere Rails was generating a full URL, the non-SSL version was being generated. It took me a bit to notice this since in most places I was using the _path version of the named routes which generates just a relative path. So on a page that was served over SSL, those relative paths of course led to SSL pages. I should note that the site continued to “work” since the generated non-SSL URL was then redirected to the SSL version by nginx, but I definitely did not want to continue to generate extra requests (it also caused a problem with some URLs when accessed via jQuery Mobile).

It took quite a lot of searching to finally find the answer; most of the things written about Rails and SSL are in relation to either 1) having Rails do the SSL redirection using something like config.force_ssl = true or 2) managing a site with some pages served over SSL and some not. The few references I found mentioned Rails using whatever scheme was in use by the current page when generating URLs (which intuitively made sense to me since it got the host name correct).

I finally ended up debugging in to the Rails source to see how it makes the decision on what scheme to use when generating URLs. After a few layers, it comes down to this code in Rack:

request.rb source
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def scheme
  if @env['HTTPS'] == 'on'
    'https'
  elsif @env['HTTP_X_FORWARDED_SSL'] == 'on'
    'https'
  elsif @env['HTTP_X_FORWARDED_SCHEME']
    @env['HTTP_X_FORWARDED_SCHEME']
  elsif @env['HTTP_X_FORWARDED_PROTO']
    @env['HTTP_X_FORWARDED_PROTO'].split(',')[0]
  else
    @env["rack.url_scheme"]
  end
end

def ssl?
  scheme == 'https'
end

Rails uses the ssl? method to determine what scheme to use in generated URLs. When I debugged my site running under SSL, none of the env variables were set, causing Rails to treat the request as if it came in over regular http.

The solution turns out to be simple (as they often are when you can track down the root cause). Add an extra header in the location configuration in nginx:

nginx.conf
1
2
3
4
5
6
7
location @app {
  proxy_set_header    X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header    X-Forwarded-Proto https; # New header for SSL
  proxy_set_header    Host $http_host;
  proxy_redirect      off;
  proxy_pass          http://unicorn_something_server;
}

After adding that single line and restarting nginx, Rails is generating URLs using https instead of http. In hindsight, it makes sense that Rails was not seeing the right scheme since the SSL is being terminated at nginx, but it never occurred to me that that’s what might be happening.