Setting Up Rails Over HTTPS Using Nginx and Let's Encrypt
HTTPS can be a bit daunting to people who have never set it up before. Luckily, systems like Nginx and Let's Encrypt have lowered the barrier to entry quite a bit. Here are all the steps you will need to set up an HTTPS connection to your Rails app.
Also, in the interest of brevity, you should reload your Nginx after each of the changes detailed below. They will not take effect until you do so.
1. Basic Nginx setup
We will start with a basic Nginx setup to proxy traffic to your Rails app. How you serve Rails is up to you. Using something like Puma, you can have Rails respond to those requests fairly easily.
First, open the file we will store our site configuration for the rest of this setup.
sudo vim /etc/nginx/sites-available/mysite
Into that file, add the following base config (yours may differ slightly based on where you store your site).
upstream mysite {
server unix:/tmp/mysite.sock;
}
server {
listen 80;
server_name mysite.com www.mysite.com;
root /var/www/mysite/public;
access_log /var/log/nginx/mysite_access.log;
error_log /var/log/nginx/mysite_error.log;
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
if ( !-f $request_filename ) {
proxy_pass http://mysite;
}
}
}
Once that is done, we will sym-link the available site in the sites-enabled
directory. This architecture allows us to host a number of sites on our server and very easily enable/disable them as we would like.
cd /etc/nginx/sites-enabled
sudo ln -s ../sites-available/mysite .
2. Let's Encrypt certificate setup
Next we will install the Let's Encrypt server client and use it to generate the certificates for our HTTPS connection.
sudo git clone https://github.com/letsencrypt/letsencrypt /opt/letsencrypt
/opt/letsencrypt/letsencrypt-auto
/opt/letsencrypt/letsencrypt-auto certonly --webroot -w /var/www/mysite/public -d 'mysite.com,www.mysite.com'
3. Add HTTPS support to Nginx
With our certificates generated, we need to update our Nginx site configuration to respect port 443 instead of 80. We also need to add the paths to our certificate and key files we generated.
ssl_certificate /etc/letsencrypt/live/mysite.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mysite.com/privkey.pem;
server {
listen 443 ssl;
server_name mysite.com www.mysite.com;
4. Celebrate! We have an HTTPS Rails app
"What? Really??" Yes.
If you point your web browser to https://www.mysite.com, you should see your site with the fancy green lock icon.
Don't celebrate too much though, we still have a number of niceties we can add to bring this setup to the next level.
5. Add SEO-focused redirects
Next will be the oft-ignored housekeeping of standardizing around a www vs a non-www domain name. Ignore the pedantry and pick whichever you would like, but be sure to force all traffic to it.
Here we will handle 2 types of redirects: http => https and non-www => www. Also, note that with the redirects being added, we must also tell our initial server
block to no longer respect the non-www domain.
# Our shared SSL credentials for our site
ssl_certificate /etc/letsencrypt/live/mysite.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mysite.com/privkey.pem;
# Redirect all http traffic to https
server {
listen 80;
server_name mysite.com www.mysite.com;
return 301 https://www.mysite.com$request_uri;
}
# Redirect other non-www traffic to www
server {
listen 443 ssl;
server_name mysite.com;
return 301 https://www.mysite.com$request_uri;
}
# Only respect https + www traffic
server {
listen 443 ssl;
server_name www.mysite.com;
# ...
}
6. Nginx SSL Security Improvements
Now it is time to add some improved security measures. The security landscape is an arms race between researchers and attackers. Here we will add some security and optimization improvements to avoid many of the vulnerabilities discovered over the past few years.
All the code you need is in this first block. If you are like me and would like to know what the code you type in actually does, i have written out line-by-line descriptions below.
server {
listen 443 ssl;
server_name www.mysite.com;
# ...
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
ssl_prefer_server_ciphers on;
ssl_stapling on;
ssl_stapling_verify on;
ssl_session_cache shared:SSL:128m;
ssl_session_timeout 120m;
Each of these lines are independently the result of some fascinating security research and development. Below are some basic descriptions about each.
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
The TLS protocols are considerably more secure than the older SSL protocols.
ssl_ciphers EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
ssl_prefer_server_ciphers on;
Along similar lines, these are the ciphers we tell Nginx to respect. Here we are selecting ciphers with a good balance of device support while not sacrificing security.
ssl_stapling on;
ssl_stapling_verify on;
This enables OSCP Stapling. "What?" This tells Nginx to provide our HTTPS certificate along with proof of its validity. This means that our visitors will not have to contact a certificate authority themselves. Fewer web requests means faster loads. On mobile, this can be a considerable time savings.
ssl_session_cache shared:SSL:128m;
ssl_session_timeout 120m;
Just as enabling stapling
speeds up things for our visitors, session caching speeds up session confirmation for Nginx. Here we are allotting Nginx a 120 MB cache, and telling it to maintain session's in a cache for 2 hours. You can (and should) play with these numbers on your system to adjust performance as you see fit. After all, what are knobs for?
7. Celebrate (again)! We have some kickass HTTPS
If we head over to Qualys's SSL Labs we can run our site against their very robust SSL tester. Do that, and we should have some fine grades.
While our base SSL setup was a giant step above raw HTTP, the improvements in step 6 really kick the security of our site up another notch.
8. Nginx Header improvements
Finally, we can achieve additional security improvements in the headers we send with our page responses. Start by appending the following to our Nginx server block.
resolver 8.8.8.8;
add_header X-Xss-Protection "1";
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;
add_header Strict-Transport-Security "max-age=31557600";
# Read about this before including!
add_header Content-Security-Policy "default-src 'self' googleapis.com";
These headers bring along a series of security enhancements. As with the ssl improvements, each of these is backed by a shocking amount of time and dev. Below are some basic descriptions of each.
resolver 8.8.8.8;
Tell Nginx that we will use Google's DNS (or any DNS you would like). For those unaware, 8.8.8.8
is the IP Address of Google's public DNS servers.
add_header X-Xss-Protection "1";
Verbosely instruct browsers to block Cross-side scripting on this site.
add_header X-Frame-Options SAMEORIGIN;
This disables your site from being placed into an iframe
. Why does that matter? Imagine someone putting your login page onto full page iframe, then putting their own text boxes above your login fields. Users think they are logging into your site, when in fact they are giving a hacker their username and password.
add_header X-Content-Type-Options nosniff;
Chrome and IE have a nasty habit of trying to infer the MIME type of your server's responses. While this is generally harmless, it can be risky if your site serves any user-uploaded content. This prevents a cleverly crafted executable appearing to Chrome as a harmless image.
add_header Strict-Transport-Security "max-age=31557600";
This is a header-based solution to the SEO redirects we added above. Quite honestly, i haven't found pros (or cons) to either method. Lacking a good reason not to, i have been using both.
add_header Content-Security-Policy "default-src 'self' googleapis.com";
Finally is the Content Security Policy header. For most instances i would honestly recommend omitting this header but i am including it here for verbosity's sake. This header allows you to strictly whitelist asset sources for things like 3rd party JS and CSS.
If you wish to use this header, please read into it. You will need to whitelist all domains you are currently relying on for 3rd party libraries. In this example, i am whitelisting Google's api domain, in addition to my local code.
9. Celebrate one more time! We have kickass headers.
Scott Helme has written a header tool similar to the SSL Labs tool above. If we run our site on it after reloading Nginx, we should see some promising results.
You will note that i have skipped the Public Key pins. I welcome the curious among you to read into it more, as it is a deep topic with some non-trivial development implications. Suffice to say, i do not consider it worth the tradeoff.
Closing thoughts and credit where it is due
I need to start by thanking Loic Lambiel for building letsecure.me. The site kicked off a weekend of curiosity and research on my part, digging into everything he touched on. His article served as a great launching point for this post, which would not have been possible without his work.
Also i would use this to urge people who do not host their own website on a server to start doing so. In all likelihood no one will ever see it, but there is so much value in picking up a serviceable understanding of server setup and maintenance. It is, unfortunately, one of those things that can only be learned by getting your hands dirty. Take it from me that it is much better to learn it when you don't have to then when something fails.
In closing, we have talked before about the value in learning the unnecessary things. While i would file learning production HTTPS setups in that category, it also holds some immediately value as well. For many who spend their days in Rails Land, branch out and enjoy the view.