Nginx Virtualhosts and SSL - Reliably Deploying Rails Applications: Hassle free provisioning, reliable deployment (2014)

Reliably Deploying Rails Applications: Hassle free provisioning, reliable deployment (2014)

17.0 - Nginx Virtualhosts and SSL

Overview

When a request reaches Nginx, it looks for a virtualhost which defines how and where the request should be routed. Nginx virtualhosts are stored in /etc/nginx/sites-enabled.

A Basic Virtualhost

The below virtualhost is the one included in the sample Capistrano configuration. It includes the rules necessary to route requests to a specific domain to our Rails app, including correctly handling compiled assets:

config/deploy/shared/nginx.conf.erb


1 upstream unicorn_<%= fetch(:full_app_name) %> {

2 server unix:/tmp/unicorn.<%= fetch(:full_app_name) %>.sock fail_timeout=0;

3 }

4

5 server {

6 server_name <%= fetch(:server_name) %>;

7 listen 80;

8 root <%= fetch(:deploy_to) %>/current/public;

9

10 location ^~ /assets/ {

11 gzip_static on;

12 expires max;

13 add_header Cache-Control public;

14 }

15

16 try_files $uri/index.html $uri @unicorn;

17

18 location @unicorn {

19 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

20 proxy_set_header Host $http_host;

21 proxy_redirect off;

22 proxy_pass http://unicorn_<%= fetch(:full_app_name) %>;

23 }

24

25 error_page 500 502 503 504 /500.html;

26 client_max_body_size 4G;

27 keepalive_timeout 10;

28 }


The upstream block allows us to define a server or group of servers which we can later refer to when using proxy_pass:

extract from: config/deploy/shared/nginx.conf.erb


1 upstream unicorn_<%= fetch(:full_app_name) %> {

2 server unix:/tmp/unicorn.<%= fetch(:full_app_name) %>.sock fail_timeout=0;

3 }


Here we define a single server as unicorn which points to the unix socket we’ve defined for our unicorn server in config/deploy/shared/unicorn.rb.

The server block begins by defining the port, root and server name:

extract from: config/deploy/shared/nginx.conf.erb


1 server {

2 server_name <%= fetch(:server_name) %>;

3 listen 80;

4 root <%= fetch(:deploy_to) %>/current/public;

5

6 ...

7 }


The listen directive defines the port this virtualhost applies to. In this case any request on port 80, the standard port for web http requests.

The server_name directive defines the hostname that the virtualhost represents. This will usually be a domain/ subdomain such as server_name www.example.com. You can also use wildcards here such as server_name *.example.com and list multiple entries on one line, separated by spaces such as server_name example.com *.example.com.

In the example Capistrano configuration, this is set in our stage file, for example:

extract from: config/deploy/production.rb


1 set :server_name, "www.example.com example.com"


The root directive defines the root directory for requests. So if the root directory was /home/deploy/your_app_production/current/public/ then a request for http://www.example.com/a_file.jpg would route to /home/deploy/your_app_production/current/public/a_file.jpg.

The next section defines behaviour specific to the folder containing our compiled assets:

extract from: config/deploy/shared/nginx.conf.erb


1 location ^~ /assets/ {

2 gzip_static on;

3 expires max;

4 add_header Cache-Control public;

5 }


The location directive allows us to define configuration parameters which are specific to a particular URL pattern defined by a regular expression. In this case to the folder which contains our static assets.

The gzip_static on directive specifies that any files in this directory can be served in a compressed form by appending .gz to the file.

The expires max directive enables the setting of the Expires and Cache-Control headers to values which will cause compliant browsers to cache the returned files as long as possible.

The add_header Cache-Control public directive adds a flag which is used by proxies to determine whether the file in question is safe to cache.

The next section defines the order in which files should be looked for:

extract from: config/deploy/shared/nginx.conf.erb


1 try_files $uri/index.html $uri @unicorn;


This means that it will first look for the existence of the file provided by the url with /index.html appended. This allows for urls like /blog/ to display the /blog/index.html page automatically. If that is not found then it will look for the file specified by the URL, if that is not found that in will pass the request onto the @unicorn location (explained below). This means that any valid requests for static assets will never be passed to our Rails app server.

The location @unicorn block defines the @unicorn location used to pass request back to our Rails app (which we looked at in the previous lines):

extract from: config/deploy/shared/nginx.conf.erb


1 location @unicorn {

2 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

3 proxy_set_header Host $http_host;

4 proxy_redirect off;

5 proxy_pass http://unicorn_<%= fetch(:full_app_name) %>;

6 }


This sets headers which provide additional information about the request itself and then uses proxy_pass to forward the request onto our rails application using the unicorn_full_app_name location we defined at the top of this file.

Notice that the proxy_pass destination starts with http:// and looks like any other web address. It is, and this is really powerful. In this case we’re using it to proxy requests back to our unicorn app server but we could also use it to proxy all or a subset of requests back to any other destination. This can for example be used to make a wordpress blog appear to be on the same domain as your rails app(!).

extract from: config/deploy/shared/nginx.conf.erb


1 error_page 500 502 503 504 /500.html;

2 client_max_body_size 4G;

3 keepalive_timeout 10;


The error_page directive indicates that all of the error codes listed should lead to the page at $root/500.html being displayed. Since root is the public folder of our Rails application, if we haven’t defined a custom 500 page, this will be the “We’re sorry, but something went wrong (500)” in red text page we all know and love.

Finally client_max_body_size defines the maximum size of the request body which will be accepted without an error being generated and keepalive_timeout defines how long keepalive connections will be allowed to remain open without activity before being automatically closed.

DNS Overview

A Records

It’s beyond the scope of this book to go into setting up the DNS for your domain in detail. In general, if your server_name is www.example.com then you would need to have an A Record setup pointing www at the the IP of your server.

Testing with /etc/hosts

If, for testing purposes, you need to test whether your server will work before adding the DNS entry, you can modify your local hosts file.

On both OSX and most *nix variants, you’ll find this in /etc/hosts. Editing this will require root access. You can add lines such as the following:

1 89.137.174.152 www.example.com

To make www.example.com resolve to 89.137.174.152 locally.

Forcing HTTPS

For some sites it can be desirable to only serve requests over https even when the user request http. To achieve this, replace the server block for port 80 with:

1 rewrite ^ https://$server_name$request_uri? permanent;

To automatically redirect all http:// requests to the https:// equivilent.

Adding SSL

To add an SSL Certificate you will need:

· Your SSL Certificate

· Any intermediate Certificates (see Chaining SSL Certificates below)

· Your SSL Private Key

To enable SSL in Nginx we have a section in config/deploy/shared/nginx.conf.erb which is included if we set :enable_ssl to true in a stage file (for example config/deploy/production.rb).

A majority of this file is the same as the none SSL definition, with the following differences:

This line;

extract from: config/deploy/shared/nginx.conf.erb


1 listen 443;


Means that the virtualhost applies to connections on port 443, the standard port for HTTPS connections.

These lines:

extract from: config/deploy/shared/nginx.conf.erb


1 ssl on;

2 ssl_certificate <%= fetch(:deploy_to) %>/shared/ssl_cert.crt;

3 ssl_certificate_key <%= fetch(:deploy_to) %>/shared/ssl_private_key.key;


This enables SSL and sets the location of the SSL certificate and corresponding private Key. These should not be included in source control so you’ll need to SSH into your server and create the relevant files. Both should be provided by your certificate supplier but see the note below on chaining SSL certificates.

Once you’ve added your certificates and run deploy:setup_config again to copy the updated NGinx vitualhost across, you’ll need to restart or reload nginx for the changes to be picked up.

Chaining SSL Certificates

Often when purchasing SSL certificates, you’ll be provided with your own certificate, an intermediate certificate and a root certificate. These should all be combined into a single file, for the example configuration this should be called ssl_cert.crt.

You’ll begin with three files:

· Your Certificate

· Primary Intermediate CA

· Secondary Intermediate CA

These should be combined into a single file in the order:

1 YOUR CERTIFICATE

2 SECONDARY INTERMEDIATE CA

3 PRIMARY INTERMEDIATE CA

You can do this with the cat command:

1 cat my.crt secondary.crt primary.crt > ssl_cert.crt

In the final section of this chapter, “Updating SSL Certificates”, there’s a simple bash script for automating this concatenation as part of the process of switching out old certificates for new ones.

For the purposes of setting up SSL on NGinx, it’s not necessary to understand why chaining is used however for reference, there’s a good explanation of it in this superuser answer http://superuser.com/questions/347588/how-do-ssl-chains-work.

Updating SSL Certificates

SSL Certificates will, after a pre agreed time period (often one year), expire and need to be replaced with new ones. It’s worth having reminders in your calendar for the expirations dates of any production certs as there’s nothing more embarrassing than your site throwing security warnings to all of your users because nobody remembered to renew the certificate.

Updating them is quite a simple process but one it’s easy to get wrong. For this reason I use a simple bash script to automated switching the old cert for the new one and rolling back in case it all goes wrong.

This script assumes your issuer provides you with a certificate, a secondary certificate and a primary certificate (based on certificates from DNSimple). You may need to modify the script if, for example, there’s no intermediate certificate.

The script assumes you’re starting with 4 files in /home/deploy/YOUR_APP_DIRECTORY/shared:

· my.crt - Your SSL Certificate

· secondary.crt - A secondary intermediate certificate

· primary.crt - A root certificate

· ssl_private_key.key.new - The private key used to generate the certificate

The following bash script provides a simple interface for switching in new certs and rolling back in the case that something goes wrong. The script should be stored in the same directory as the target for the certificates. In the case of our sample configuration, this is/home/deploy/your_app_environment/shared/.

1 #!/bin/bash

2

3 if [ $# -lt 1 ]

4 then

5 echo "Usage : $0 command"

6 echo "Expects: my.crt, secondary.crt, primary.crt, ssl_private_key.key\

7 .new"

8 echo "Commands:"

9 echo "load_new_certs"

10 echo "rollback_certs"

11 echo "cleanup_certs"

12 exit

13 fi

14

15 case "$1" in

16

17 load_new_certs) echo "Copying New Certs"

18 cat my.crt secondary.crt primary.crt > ssl_cert.crt.new

19

20 mv ssl_cert.crt ssl_cert.crt.old

21 mv ssl_cert.crt.new ssl_cert.crt

22

23 mv ssl_private_key.key ssl_private_key.key.old

24 mv ssl_private_key.key.new ssl_private_key.key

25

26 sudo service nginx reload

27 ;;

28 rollback_certs) echo "Rolling Back to Old Certs"

29 mv ssl_cert.crt ssl_cert.crt.new

30 mv ssl_cert.crt.old ssl_cert.crt

31

32 mv ssl_private_key.key ssl_private_key.key.new

33 mv ssl_private_key.key.old ssl_private_key.key

34

35 sudo service nginx reload

36 ;;

37 cleanup_certs) echo "Cleaning Up Temporary Files"

38 rm ssl_cert.crt.old

39 rm ssl_private_key.key.old

40 rm my.crt

41 rm secondary.crt

42 rm primary.crt

43 ;;

44 *) echo "Command not known"

45 ;;

46 esac

Don’t forget to make the script executable with chmod +x script_name.sh.

You can then simply run:

1 ./script_name load_new_certs

to swap in the new certificates and reload nginx. If, after testing the site, something isn’t right, you can execute:

1 ./script_name rollback_certs

To revert to the previous ones. And then repeat load_new_certs once you’ve resolved the issue.

Once you have the new certificates working as intended, you can use:

1 ./script_name cleanup_certs

To remove the temporary and legacy files created.