A guide on implementing free SSL certificate on Nginx using Let's Encrypt

If you're reading this article, I'm assuming that you're already sold on the numerous benefits of securing your website with an SSL certificate. Wether it's for encrypting data transfers, establishing more credibility or even improving your SEO ranking (yes you heard it right) it's now easier than ever to obtain an SSL certificate for that.

Traditionally, an SSL certificate costs anywhere from $30 to $100 a year depending on where you're hosting it. It has always been a struggle to find a truly reliable authority for obtaining a free SSL.

Luckily, the Linux Foundation decided to step up and solve this problem by creating Let's Encyrpt, a free, automated, and open CA (Certificate Authority) for everyone. With Let's Encrypt, you can now encrypt everything for free.

Let's Encrypt is a fantastic service, but with the vast amount of 3rd party tutorials out there and so many changes that happened with the service itself through the different phases, I found most of the content either confusing or just obsolete. In the end, and after a lot of research from here and there, I managed to get it working.

The process itself turned out to be pretty simple, so I've summarised a full tutorial on how to do it on Ubuntu 16.04 with Nginx in 4 straightforward steps. The steps should be doable under other Linux flavours or Ubuntu versions, with small differences in the commands.

Some background about Let's Encrypt

Before diving into the process I want to highlight a couple of details on the implementation.

The basic idea for identifying domain control in order to issue or renew certificates is to serve letsencrypt "ACME Challenges" from your web server.

There are mainly 2 modes of operation that you can setup your certificate on:

  1. Standalone

    This mode is used to obtain a certificate if you don't want to use or already have a web server setup. It does not rely on the web server software (such as Nginx) to identify the domain and obtain a certificate. In that case, letsencrypt would have to bind to port 80 and serve the challenges.

    More details about stand alone mode here

  2. Webroot

    This mode allows you to serve the challenges from your locally running web server (such as Nginx) from a known folder. You don't need to stop the server during the issuance or renewal process while it's already bound to port 80. More details will have to included related to your project web root paths.

    The great thing about this mode is that it's straightforward to setup wether you have an existing web server installed or a installing a new one.

    More details about Webroot mode here

For more information on how Let's Encrypt works, you check out this link

In this guide I'll be going with the webroot mode with Nginx, so let's dive in!

Step 1: Setup and prepare Nginx

First of all, we'll install and setup all the files related to Nginx and the base configuration, if you don't have a web server installed yet now is the time to do so.

sudo apt-get install nginx

Now that we have Nginx, lets setup 2 main config files that can be included into our main Nginx config later. Optionally, you could jusr include those sections directly into your main Nginx site config file but it's more flexible to separate and include them on demand. The files we'll be creating below can be re-used with multiple Nginx site configurations.

Let's encrypt challenge

As mentioned earlier, we need to create a hidden directory and configure our web server to serve the challenges from it, in order for the certificate authority to validate the domain.

Let's create the file

/etc/nginx/snippets/letsencrypt.conf

and add the following configuration into it

location ^~ /.well-known/acme-challenge/ {
    default_type "text/plain";
    root /var/www/letsencrypt;
}

Then we can create an empty challenges directory, that's where Let's Ecnrypt will be adding the challenges to later on.

sudo mkdir -p /var/www/letsencrypt/.well-known/acme-challenge

Now that we have /etc/nginx/snippets/letsencrypt.conf available, we can later plug it into any individual Nginx website configuration and have it serve challenges for that domain on the web server.

SSL configuration

In order to run the web server on SSL, we need to provide some configuration related to the SSL certificate such as encryption type, headers, protocols etc...

This configuration can also be added into a separate file, and later on plugged into any Nginx configuration that will use Let's Encrypt certificate.

Lets create the file

/etc/nginx/snippets/ssl.conf

with the following contents

ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;

ssl_protocols TLSv1.2;
ssl_ciphers EECDH+AESGCM:EECDH+AES;
ssl_ecdh_curve secp384r1;
ssl_prefer_server_ciphers on;

ssl_stapling on;
ssl_stapling_verify on;

add_header Strict-Transport-Security "max-age=15768000; includeSubdomains; preload";
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;

Nginx basic configuration

Once the above 2 files are ready, we can now include them in any website specific Nginx configuration.

We'll first need set up a basic Nginx config (HTTP only) on our website domain in order to serve the challenges and identify the domain name ownership. We'll later on build up on this same Nginx config file for enabling SSL after obtaining the SSL certificate in step 2.

If you already have an established website being served on HTTP, you probably have a very similar setup to the below. All you will need to do in that case is to just reference include /etc/nginx/snippets/letsencrypt.conf; in your existing config file and restart your web server. Otherwise, you can create a config from scratch as specified below.

Create your Nginx config file

/etc/nginx/sites-available/mywebsite.conf

With the contents similar to

server {
    listen 80 default_server;
    listen [::]:80 default_server ipv6only=on;
    server_name mywebsite.com www.mywebsite.com;

    include /etc/nginx/snippets/letsencrypt.conf;

    root /var/www/mywebsite;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

Enable the config file

ln -s /etc/nginx/sites-available/mywebsite.conf /etc/nginx/sites-enabled/mywebsite.conf

Restart the web server

sudo service nginx restart

Once this is done, the web server should be able to serve challenges under .well-known/acme-challenge through your domain name. As you have noticed, this is after including letsecrypt.conf into our domain's config.

Step 2: Install and generate a Let's Encrypt certificate

Now that the challenges can be served under the domain, we'll be able to verify the ownership. Proceed with installing Let's Encrypt and obtaining a certificate.

Install Let's Encrypt via

sudo apt-get install letsencrypt

Obtain the certificate via

letsencrypt certonly --webroot -w /var/www/letsencrypt -d www.mywebsite.com -d mywebsite.com --email your@email.com --agree-tos

Here is a breakdown explaining the command parameters above:

  • -w /var/www/letsencrypt Specifies the root directory that we're serving your website challenges from
  • --webroot Specifies that we're using the web root mode
  • -d www.mywebsite.com Specifies the domain name we're issuing the certificate for, it can be multiple ones as it's above with www and without www
  • --email your@email.com Specifies the working email address to to associate with this certificate.

After running the command, the SSL related keys and files will be available under

/etc/letsencrypt/live/www.mywebsite.com/

For more information about the letsencrypt command check out the detailed official docs.

Step 3: Integrate the certificate into the config

Now that the certificate and Nginx domain config are ready, all we have to do is link to the certificate of the domain from the site's config. Open up /etc/nginx/sites-available/mywebsite.conf.

Adding the contents below to the file will enable SSL on the www version of the domain.

 server {
    server_name www.mywebsite.com;
    listen 443 ssl http2 default_server;
    listen [::]:443 ssl http2 default_server ipv6only=on;

    ssl_certificate /etc/letsencrypt/live/www.mywebsite.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/www.mywebsite.com/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/www.mywebsite.com/fullchain.pem;
    include /etc/nginx/snippets/ssl.conf;

    root /var/www/mywebsite.com;
    index index.html;
    location / {
        try_files $uri $uri/ =404;
    }
}

The above domain configuration is very similar to the initial one, the difference is that

  • It listens on port 443 (the default port for SSL)
  • It's for the www version of the domain
  • It includes the ssl.conf file previously created in step 1 and links the generated certificate via ssl_certificate, ssl_certificate_key, and ssl_trusted_certificate properties.

Restarting the web server via sudo service nginx restart will enable this configuration and allow us to access the website via https://www.mywebsite.com hurray! :)

One important thing to notice is that the site is still accessible via non www and non https URls such as

  • http://mywebsite.com
  • http://www.mywebsite.com

And https://mywebsite.com is not even accessible.

It's usually a best practice (especially for SEO) to just force https on your domain and use only one style either with www or without www. This can be easily done through 301 redirects on those variations.

First of all update the initially created non https config to redirect all requests to https://www.mywebsite.com via

server {
    listen 80 default_server;
    listen [::]:80 default_server ipv6only=on;
    server_name mydomain.com www.mydomain.com;

    include /etc/nginx/snippets/letsencrypt.conf;

    location / {
        return 301 https://www.mydomain.com$request_uri;
    }
}

Then also make sure to redirect non www https to www by adding

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name mydomain.com;

    ssl_certificate /etc/letsencrypt/live/www.mydomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/www.mydomain.com/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/www.mydomain.com/fullchain.pem;
    include /etc/nginx/snippets/ssl.conf;

    location / {
        return 301 https://www.mydomain.com$request_uri;
    }
}

Once the web server is restarted, we'd have https://wwww. serving the website by default. Congrats!

Step 4: Renewing certificates

A Let’s Encrypt certificates is valid for 90 days after being issues, so it needs to be renewed often. The renewal process is pretty simple, all we have to do is run

letsencrypt renew --pre-hook "service nginx stop" --post-hook "service nginx start"

This command will renew certificates expiring in less than 30 days. Notice the pre-hook and post-hook, those are the commands we want to be running before and after renewal, which in this case is stopping and restarting Nginx.

For more details on renewing certificates command check out the docs here

It would definitely be annoying to login to the server constantly to update the certificates. One thing we can do is add a cronjob to do it for us, open up the crontab file

crontab -e

and include the following line

0 0 1 * * letsencrypt renew --pre-hook "service nginx stop" --post-hook "service nginx start >> /var/log/letsencrypt.log

This sets the renewal command to run on the first of every month at 12:00AM, updating any certificates that are expiring in less than 30 days.

After this step, your website should be all set and running on Let's Encrypt.

Delivering more secure web products is crucial for your users. End users are already demanding the availability of secure web pages. As a matter of fact, Google Chrome is going to eventually label all HTTP pages as non-secure in very obvious ways for users.

So what are you waiting for? Let's Encrypt!

Enjoyed this post? Help me spread the word and let me know your feedback!
Fork me on GitHub