Configuring Nginx and Unicorn for force_ssl

It turns out that setting up SSL on Nginx + Unicorn + Rails is actually pretty easy. But there's a few pitfalls you have to watch out for. The following guide is based partially on these instructions and assumes you already have an SSL certificate and already have it placed on your server.

Let's take a look at our initial Nginx configuration file. You can find yours in /etc/nginx/sites-available, but if you're reading this you probably already knew that.

upstream unicorn_mysite {
	server unix:/tmp/unicorn.mysite.sock fail_timeout=0;
}

server {
	listen 80;
	server_name mysite.com;
	root /srv/mysite/current/public;
	access_log /srv/mysite/shared/log/nginx.access.log main;
	error_log /srv/mysite/shared/log/nginx.error.log info;

	location ^~ /assets/ {
		gzip_static on;
		expires max;
		add_header Cache-Control public;
	}

	try_files $uri/index.html $uri @unicorn;
	location @unicorn {
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		proxy_set_header Host $http_host;
		proxy_redirect off;
		proxy_pass http://unicorn_mysite;
	}

	error_page 500 502 503 504 /500.html;
	client_max_body_size 4G;
	keepalive_timeout 10;
}

As you can see, this configuration makes some assumptions about our setup that are unlikely to be true for yours. However, for this exercise the details of the configuration are largely inconsequential.

In your editor of choice take the above config file and copy the server section and paste it below. Now, make the second server section look something like this:

server {
	listen 443;
	ssl on;
	ssl_certificate /etc/httpd/conf/ssl/mysite.com.crt;
	ssl_certificate_key /etc/httpd/conf/ssl/mysite.com.key;

	server_name mysite.com;
	root /srv/mysite/current/public;
	access_log /srv/mysite/shared/log/nginx.ssl.access.log main;
	error_log /srv/mysite/shared/log/nginx.ssl.error.log info;

	location ^~ /assets/ {
		gzip_static on;
		expires max;
		add_header Cache-Control public;
	}

	try_files $uri/index.html $uri @unicorn;
	location @unicorn {
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		proxy_set_header X-Forwarded-Proto https;
		proxy_set_header Host $http_host;
		proxy_redirect off;
		proxy_pass http://unicorn_mysite;
	}

	error_page 500 502 503 504 /500.html;
	client_max_body_size 4G;
	keepalive_timeout 10;
}

The first difference you should notice is the listen port. HTTPS uses port 443 instead of port 80. The following three lines tell Nginx that we want SSL on and where our certificate and where our certificate keys are being stored. /etc/httpd/conf/ssl is a pretty standard location, but you can keep them anywhere.

The next change we make is to the log file locations. The normal HTTP config will write to nginx.access.log and nginx.error.log. Here we're telling the HTTPS config to write to nginx.ssl.access.log and nginx.ssl.error.log instead. If you ever encounter any problems with your SSL setup it'll be pretty handy to have your logs separated out by protocol.

The last difference between the two configurations is the extra proxy_set_header setting. Since we plan on using force_ssl in our Rails application to selectively ensure SSL on different controllers this step is really important. force_ssl relies on the HTTP_X_FORWARDED_PROTO HTTP header to determine whether or not the request was an HTTPS request. If this setting isn't set to https then you will end up with an infinite redirect loop as force_ssl will always think the forwarded request isn't HTTPS.

At this point you should restart Nginx: sudo /etc/init.d/nginx restart. In your Rails app's controller add the call to force_ssl at the top like this:

class ContactsController < ApplicationController
  force_ssl
  before_filter :whatever
  ...

Now, when you go to any action on that controller you should immediately be redirected to the SSL version.

If you get an error similar to "Error 102 (net:: ERR CONNECTION REFUSED)" then this likely means your server is blocking port 443. Odds are you won't have this issue, but I did, so it makes sense to me to include a possible fix. This makes the assumption you're using iptables to manage your ports. Open up /etc/sysconfig/iptables and look for a line similar to this:

-A INPUT -m state --state NEW -m tcp -p tcp --dport 80 -j ACCEPT

Immediately below it add the following:

-A INPUT -m state --state NEW -m tcp -p tcp --dport 443 -j ACCEPT

As usual, if your settings look similar but not quite the same then base your changes off your settings. The important part here is the --dport, we want to open up port 443. After you do this you'll need to restart iptables with sudo /etc/init.d/iptables restart.

At this point your controllers with force_ssl in them should be redirecting to the SSL version of your site. Like most ActionController callbacks you can also specify which actions force_ssl will be run on using the only and except options.