< Return to Blog

HOWTO: Setting up HTTPS (SSL/TLS) with Nginx & Rails

In this article, I dive into the intricacies of creating a self-signed SSL certificate for local development & testing, working with CA-bundles (intermediate certificates) when purchasing your production SSL certificate, and how to configure Nginx so that its SSL options are configured for improved security (and performance).

I also provide an Nginx configuration that I am currently using in production for a client's eCommerce application.

Creating a self-signed Certificate

The following is a summary of the steps detailed in the Heroku Devcenter. I will first go over this step-by-step and then show you how it could be done as a one-liner.

Generate private key

A private key and certificate signing request are required to create an SSL certificate.

Start by creating the private server key. During this process, you are normally asked to enter a specific passphrase, which we have skipped by passing this via the -passout pass:x option, where 'x' is passed as the password.

$ openssl genrsa -des3 -passout pass:x -out server.pass.key 2048

Remove the Passphrase

It would serve us to remove the passphrase. Although having the passphrase in place does provide heightened security, the issue starts when one tries to reload Nginx. In the event that Nginx crashes or needs to reboot, you will always have to re-enter your passphrase to get your entire web server back online.

Although we've deleted server.pass.key you could opt to keep it stored safely.

Use this command to remove the password:

$ openssl rsa -passin pass:x -in server.pass.key -out server.key
writing RSA key
$ rm server.pass.key

Generate certificate signing request

Follow up by creating a certificate signing request:

$ openssl req -new -key server.key -out server.csr

This command will prompt terminal to display a lists of fields that need to be filled in.

The most important line is "Common Name". Enter your official domain name here or, if you don't have one yet, your site's IP address.

Leave the challenge password and optional company name blank.

You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:US
State or Province Name (full name) [Some-State]:New York
Locality Name (eg, city) []:NYC
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Awesome Inc
Organizational Unit Name (eg, section) []:Dept of Merriment
Common Name (e.g. server FQDN or YOUR name) []:example.com                  
Email Address []:webmaster@awesomeinc.com
...

A challenge password []:
...

You will be left with a private key server.key and certificate signing request server.csr.

Generate self-signed SSL certificate

Your certificate is all but done, and you just have to sign it using the server.key private key and server.csr files.

Keep in mind that you can specify how long the certificate should remain valid by changing the 365 to the number of days you prefer. As it stands this certificate will expire after one year.

$ openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt

The server.crt file is your site certificate along with the server.key private key; these can be named as you wish, although I typically use <app-name>-<env> as a naming scheme as this fits nicely with my Docker containers.

In a hurry?

Alternatively, we could create the SSL key and certificate files in one motion via

$ openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/nginx/ssl/nginx.key -out /etc/nginx/ssl/nginx.crt

It's worth taking a look at the article in the DigitalOcean knowledge base as it details the various options passed above.

Dealing with Intermediate SSL Certificates, when purchasing your certificate

When purchasing a SSL certificate, the certificate authority will give you a bunch of files, that may look like these or those below

yourDomain.ca-bundle
yourDomain.crt
yourDomain.p7b

The *.crt file above is your certificate, keyed for your domain, however, the intermediate certificates are in the *.ca-bundle; these need to be concatenated

cat yourDomain.crt  yourDomain.ca-bundle >> yourDomain-ca-bundle.crt

You only need to give the yourDomain-ca-bundle.crt bundle and your private *.key key file to Nginx.

You can purchase SSL certificates for your domain, from vendors such as SSLs — I am in no way affiliated with SSLs, just a happy customer; they even have one for $8! It's generally a good idea to shop around.

Configuring Nginx for SSL/TLS

Much of the improved security and performance configuration, specifically for SSL, are based on an excellent gist and further links are detailed in the config itself. My goal in curating this configuration was to bake in the latest and best security options, whilst ensuring there's a sense of pragmatic compatibility for as many connecting clients.

This configuration is one that I've made for Crestwood.co.uk and is designed to work with Unicorn, whilst allowing HTTP connectivity as well.

The configuration is applied to the app node via Ansible, and as such, those familiar with its use of Jinja2 for templating will also be familiar with the 'double-handlebars' — when the Ansible play runs these will be replaced with values applicable to the app instance being deployed.

# NOTE: Based on https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
# and https://gist.github.com/plentz/6737338
# and http://tautt.com/best-nginx-configuration-for-security/

upstream {{unicorn_server}} {
  server unix:/tmp/{{unicorn_socket}} fail_timeout=0;
}

server {
  listen 80;
  listen 443 ssl;
  server_name {{server_name}};

  ssl_certificate {{ nginx_ssl_keys_dir }}/{{ app_name }}.crt;
  ssl_certificate_key {{ nginx_ssl_keys_dir }}/{{ app_name }}.key;

  # Enable session resumption to improve https performance
  # http://vincent.bernat.im/en/blog/2011-ssl-session-reuse-rfc5077.html
  ssl_session_cache shared:SSL:50m;
  ssl_session_timeout 5m;

  # Diffie-Hellman parameter for DHE ciphersuites, recommended 2048 bits
  ssl_dhparam {{ nginx_ssl_dhparam }};

  # Enables server-side protection from BEAST attacks
  ssl_prefer_server_ciphers on;
  # Disable SSLv3(enabled by default since nginx 0.8.19) since it's less secure then TLS http://en.wikipedia.org/wiki/Secure_Sockets_Layer#SSL_3.0
  ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
  # ciphers chosen for forward secrecy and compatibility via
  # https://mozilla.github.io/server-side-tls/ssl-config-generator/
  ssl_ciphers "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA";

  # Don't allow the browser to render the page inside an frame or iframe
  # if you need to allow [i]frames, you can use SAMEORIGIN or even set an uri with ALLOW-FROM uri
  # https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options
  add_header X-Frame-Options SAMEORIGIN;

  root {{deploy_dir}}/public;
  try_files $uri @{{unicorn_server}};

  large_client_header_buffers 4 32k;
  client_max_body_size 50M;

  location @{{unicorn_server}} {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_redirect off;
    proxy_pass http://{{unicorn_server}};

    proxy_connect_timeout       900;
    proxy_send_timeout          900;
    proxy_read_timeout          900;
    send_timeout                900;
  }

  location ~ "^/assets/.+-([0-9a-f]){32}\.(jpg|jpeg|gif|css|png|js|ico|svg|woff|ttf|eot|map)(\.gz)?" {
    gzip_static on;
    expires max;
    add_header Cache-Control public;
    add_header Last-Modified "";
    add_header ETag "";

    open_file_cache max=1000 inactive=500s;
    open_file_cache_valid 600s;
    open_file_cache_errors on;
    break;
  }

}

Mozilla's SSL Configuration Generator aided in ensuring the SSL ciphers used were compatible, with a pragmatic level of legacy support.

Following from Heartbleed, I wanted to make sure this SSL config guarded against various attack vectors such as FREAK and LogJAM. You'll notice I have taken steps against LogJAM in particular; the Diffie-Hellman parameter for DHE ciphersuites is also generated via openssl dhparam -out /etc/nginx/ssl/dhparam.pem 2048 and templated via Ansible.

For a list of the various SSL exploits and a lot more in depth information, this article is a must read as it touches on

This tutorial shows you how to set up strong SSL security on the nginx webserver. We do this by updating OpenSSL to the latest version to mitigate attacks like Heartbleed, disabling SSL Compression and EXPORT ciphers to mitigate attacks like FREAK, CRIME and LogJAM, disabling SSLv3 and below because of vulnerabilities in the protocol and we will set up a strong ciphersuite that enables Forward Secrecy when possible. We also enable HSTS and HPKP. This way we have a strong and future proof ssl configuration and we get an A+ on the Qually Labs SSL Test.

Once deployed, it is advisable to run Qualys SSL Labs’ SSL Server Test as this provides a detailed report on your SSL configuration, which clients are affected by the ciphers chosen, and any attacks you may still be open to.

Want Strong SSL Security for Modern Browsers?

If you want to gain an A+ rating on Qualys SSL Labs’ SSL Server Test, consider:

Here's a summary of changes that would need to be made to the Nginx config I previously provided to implement the changes listed above, whilst leaving out the optional HPKP.

  # strong Forward Secrecy enabled ciphersuite
    # https://cipherli.st/
    ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";

    # enable ocsp stapling (mechanism by which a site can convey certificate revocation information to visitors in a privacy-preserving, scalable manner)
    # http://blog.mozilla.org/security/2013/07/29/ocsp-stapling-in-firefox/
    resolver 8.8.8.8;
    ssl_stapling on;
    ssl_trusted_certificate /etc/nginx/ssl/star_forgott_com.crt;

    # config to enable HSTS(HTTP Strict Transport Security) https://developer.mozilla.org/en-US/docs/Security/HTTP_Strict_Transport_Security
    # to avoid ssl stripping https://en.wikipedia.org/wiki/SSL_stripping#SSL_stripping
    add_header Strict-Transport-Security "max-age=31536000; includeSubdomains;";

    # when serving user-supplied content, include a X-Content-Type-Options: nosniff header along with the Content-Type: header,
    # to disable content-type sniffing on some browsers.
    # https://www.owasp.org/index.php/List_of_useful_HTTP_headers
    # currently suppoorted in IE > 8 http://blogs.msdn.com/b/ie/archive/2008/09/02/ie8-security-part-vi-beta-2-update.aspx
    # http://msdn.microsoft.com/en-us/library/ie/gg622941(v=vs.85).aspx
    # 'soon' on Firefox https://bugzilla.mozilla.org/show_bug.cgi?id=471020
    add_header X-Content-Type-Options nosniff;

    # don't send the nginx version number in error pages and Server header
    server_tokens off;

Strong SSL Only

Chances are you'd prefer to only allow SSL access to your app, in which case you'll need to make the following changes, in addition to the changes I've mentioned above for modern browsers.

  1. Remove the listen 80; directive within the server { } block. Just keep the listen 443 ssl; directive.

  2. Add a second server block to redirect HTTP traffic

# redirect all http traffic to https
server {
  listen 80;
  server_name {{server_name}};
  return 301 https://$host$request_uri;
}

SSL & Rails

As of Rails 4, SSL can be forced on all controllers by setting config.force_ssl = true in your production environment, which also enforces HTTP Strict Transport Security (HSTS) and secure (signed) cookies.

You can also call force_ssl within select controllers, to lock them down selectively. However, you are open to session hijacking (or also known as sidejacking) when allowing both HTTPS & HTTP connections.

Thwarting Session Hijacking (Sidejacking)

This is in particular a very real concern, when allowing a site to be accessed via HTTPS & HTTP; even if a user logs in via HTTPS, as soon as they send traffic via HTTP, their cookies are exposed and can be used to hijack their sessions.

The best solution against this, is to set config.force_ssl = true in your production environment, so as to lock every controller down with SSL, forcing HTTP Strict Transport Security (HSTS) and secure (signed) cookies — end-to-end.

However, if you do wish to allow both HTTPS/HTTP connectivity, then measures need to be taken to guard against session hijacking, and this is best detailed in Railscast #356 Dangers of Session Hijacking, which I will summarise below.

Assuming the user ID is stored in the session as user_id, we need to store a signed cookie along side the regular session, which can easily be done as cookies.signed[:secure_user_id] = {secure: true, value: "secure#{user.id}"}.

Whenever we fetch the current user, we can check that either the current request isn’t over a secure connection or, if it is, that the secure_user_id cookie has the correct value.

def current_user
  if !request.ssl? || cookies.signed[:secure_user_id] == "secure#{session[:user_id]}"
    @current_user ||= User.find(session[:user_id]) if session[:user_id]
  end
end

With this technique a hijacker can still act as another user on a normal HTTP request but for HTTPS requests they won’t be allowed access. This means that we should be sure to enforce SSL on pages that we don’t want potential hijackers to access.