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:
- using a strong cipher suite
- enabling HTTP Strict Transport Security (HSTS)
- enabling OCSP Stapling
- enabling HTTP Public Key Pinning Extension (HPKP) (optional)
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.
Remove the
listen 80;
directive within theserver { }
block. Just keep thelisten 443 ssl;
directive.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.