LetsEncrypt and NginX

Written on February 5, 2016

A little while ago, LetsEncrypt has been released. LetsEncrypt is extremely easy to setup on Centos and Ubuntu in combination with Nginx. At the end your domain names will have SSL and get an A* SSL rating. For Free.

We are assuming that the following is already present on the both the Centos and the Ubuntu machine:

  • Nginx is installed and running
  • There is a node-app running that Nginx points to
  • Nginx is configured for a domain and that node-app.

Installing LetsEncrypt and generating the keys

To start, install LetsEncrypt using

sudo apt-get install letsencrypt

To create the certificates for your domain first stop Nginx

sudo systemctl stop nginx

Now run LetsEncrypt and have it generate the required files for you. For this to work you need to have the (sub-)domain point to this specific server.

sudo letsencrypt certonly --standalone

It will generate the files (four in total) in the folder /etc/letsencrypt/live/your.domain.com/. Where your.domain.com resembles your domain.

Configuring Nginx

Now we need to configure Nginx and then restart it to take effect. Open the Nginx conf file for your domain (which is probably in a path like: /etc/nginx/sites-available/your.domain.com.conf) and make sure it looks similar to:

# Base.
server {
listen 80;
listen [::]:80 default_server ipv6only=on;
return 301 https://$host$request_uri;
}

# HTTPS - proxy requests on to local Node.js app
server {
        listen 443;
server_name your.domain.com;
        ssl on;

# Use certificate and key provided by Let's Encrypt:
ssl_certificate /etc/letsencrypt/live/your.domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your.domain.com/privkey.pem;
        ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';
        # Pass requests for / to localhost:3000:
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://localhost:3000/;
proxy_ssl_session_reuse off;
proxy_set_header Host $http_host;
proxy_cache_bypass $http_upgrade;
proxy_redirect off;
}
}

The things to change are the following three lines where you need to change your.domain.com for your actual domain

        server_name your.domain.com;
...
        # Use certificate and key provided by Let's Encrypt:
ssl_certificate /etc/letsencrypt/live/your.domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your.domain.com/privkey.pem;

Now test the Nginx configuration and then start it back up

# Ubuntu
sudo nginx -t
# Ubuntu
sudo systemctl start nginx

Note. Make sure Nginx allows for HTTPS

# Ubuntu
sudo ufw allow 'Nginx Full'

Additional Settings

Diffie-Hellman Parameters

One thing that is strongly recommended is to generate your own Diffie-Hellman parameters. This is not listed in the security headers, but we’ll do it anyways for good practice. If you don’t you will use the default parameters when communicating with clients securely and these are considered weak(er). To generate new parameters use:

Note: Generating the new parameters will/can take some time, you may want to grab a coffee…

openssl dhparam -out /etc/letsencrypt/live/your.domain.com/dhparams.pem 4096;

Notice how we are storing these alongside the keys certificates for the domain. Inside your nginx configuration file for this domain then, you will need to add the following to use the newly created parameters

# Use own Diffie-Hellman parameters.
ssl_dhparam /etc/letsencrypt/live/your.domain.com/dhparams.pem;

OCSP Stapling

This is a performance enhancement.

OCSP stapling is a TLS/SSL extension which aims to improve the performance of SSL negotiation while maintaining visitor privacy.
ssl_stapling on;
ssl_stapling_verify on;

ssl_trusted_certificate /etc/letsencrypt/live/your.domain.com/chain.pem;
resolver 8.8.8.8 8.8.4.4 valid=86400;
resolver_timeout 10;

Almost done

There is good news and there is bad news. The good news is obviously that you now have a site that is only approachable via HTTPS and an SSL certificate to serve it. So all seems good. However, when you test your website on https://securityheaders.io you will get some bad news. It seems there is a lot wrong with the headers.

Bad news…
Bad news explained.

Lets try to tackle two of them!

HTTP Public Key Pinning (HPKP)

It is also recommended then to pin your public key. This is to make it easier for clients to validate your connection can be trusted.

To pin the certificate you need to generate a signature for your key. You will need this in base64 standard, so run the following command, replacing with your domain:

openssl x509 -pubkey < /etc/letsencrypt/live/your.domain.com/fullchain.pem | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64

To better understand what each part of this command does we recommend reading this explanation. Then add these headers to your Nginx configuration file, replacing [BASE64-KEY] with the output of the above command

# Pinning the public key.
add_header Public-Key-Pins 'pin-sha256="[BASE64-KEY]"; max-age=10; includeSubdomains';

Notice, the max-age=10. This is in seconds and is good for testing purposes. Increase to 90 days once you are ready (7776000 seconds)

To check how well its working you can check this URL.

HTTP Strict Transport Security (HSTS)

HSTS allows a site to request that it always be contacted over HTTPS. https://www.chromium.org/hsts
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

Content Security Policy

add_header Content-Security-Policy "default-src https: data: 'unsafe-inline' 'unsafe-eval'" always;

X-Frame Options

add_header X-Frame-Options "SAMEORIGIN" always;

X-XSS Protection

This header is used to configure the built in reflective XSS protection found in Internet Explorer, Chrome and Safari (Webkit). Valid settings for the header are 0, which disables the protection, 1 which enables the protection and 1; mode=block which tells the browser to block the response if it detects an attack rather than sanitising the script.
add_header X-Xss-Protection "1; mode=block" always;

X-Content-Type-Options

Nice and easy to configure, this header only has one valid value, nosniff. It prevents Google Chrome and Internet Explorer from trying to mime-sniff the content-type of a response away from the one being declared by the server. It reduces exposure to drive-by downloads and the risks of user uploaded content that, with clever naming, could be treated as a different content-type, like an executable.
add_header X-Content-Type-Options "nosniff" always;

Further Reading

Some great further notes and explanations:

Like what you read? Give Golog a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.