Installing Let's Encrypt with Ansible


I started using Let's Encrypt during beta and feel in love with it. It's made installing, setting up and maintaining SSL certificates so easy. It's also extremely affordable (given that it is free) for everyone. I truely believe every website should be running over HTTPS no matter what and Let's Encrypt gives you no more excuses. In this article I'm going to tell you how I've setup and automated Let's Encrypt on a half dozen of projects. I've set everything up on Ubuntu 14.10 and am using Nginx as my webserver. If you are working on another O/S and using another webserver this configuration may not work for you.

Installing and Setting up Nginx with Let's Encrypt

When installing Let's Encrypt I've done part of it manually and then automated, with Ansible, the renewal. The initial setup and obtaining the certificate is done by hand. Before we get started we need to install a few dependencies and Nginx on our Ubuntu server. I normally have this scripted as part of my server setup but for now let's walk through the installation steps.

The first thing we need to do is update our package manager cache

sudo apt-get update

Then we'll need to install git and bc if they aren't already installed. Git, as you know, is a version control system and we are using it to pull down the Let's Encrypt code used to obtain our SSL certs. The bc library is an interactive algebraic language with arbitrary precision which follows the POSIX 1003.2 draft standard.

sudo apt-get install -y git bc

We'll be using the web-root plugin with Nginx to obtain our SSL cert. Before setting up the web-root plugin we need to install Nginx.

sudo apt-get install -y nginx

Now that we have git, bc and Nginx installed we can configure Nginx to use the web-root plugin. The web-root plugin works by creating a temporary file in the ./well-known directory. Let's Encrypt will open the temporary file inorder to do it's validation. Lets give Nginx access to the web-root directory by editing the Nginx configuration file.

sudo vi /etc/nginx/sites-available/default

We'll need to add the following location block to the SSL server block

location ~ /.well-known {
    allow all;
}

We can now save, exit and reload nginx.

sudo /etc/init.d/nginx reload

Installing and Obtaining an SSL Certificate

Now that we have Ngnix configured to access the web-root plugin we can setup and install Let's Encrypt. The first thing we want to do is clone the Let's Encrypt source code.

sudo git clone https://github.com/letsencrypt/letsencrypt /opt/letsencrypt

We now have the Let's Encrypt source code setup and are ready to obtain our SSL certificate. We're going to use the web-root plugin to request an SSL certificate using the command line tool. We will use the -d command to specify the domain.

cd /opt/letsencrypt
sudo ./letsencrypt-auto certonly -a webroot --webroot-path=/usr/share/nginx/html -d yourdomain.com -d www.yourdomain.com

When you run the command line tool you will be prompted with a few questions. Let's encrypt will ask you for your email address (which is used for urgent notices and lost key recovery) and it will ask you to accept the terms and conditions. Assuming we have configured everything correctly and entered the correct information into the prompts we should get the below output.

IMPORTANT NOTES:
- If you lose your account credentials, you can recover through
   e-mails sent to ralph@ralphlepore.net
- Congratulations! Your certificate and chain have been saved at
   /etc/letsencrypt/live/yourdomain.com/fullchain.pem. Your
   cert will expire on 2016-03-15. To obtain a new version of the
   certificate in the future, simply run Let's Encrypt again.
- Your account credentials have been saved in your Let's Encrypt
   configuration directory at /etc/letsencrypt. You should make a
   secure backup of this folder now. This configuration directory will
   also contain certificates and private keys obtained by Let's
   Encrypt so making regular backups of this folder is ideal.
- If like Let's Encrypt, please consider supporting our work by:


Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
Donating to EFF:                    https://eff.org/donate-le

You'll be able to find your SSL certificates in

/etc/letsencrypt/live/$domain

The following files are stored in the directory

  • privkey.pem --> Your private key
  • cert.pem --> Server certificate
  • chain.pem --> Root and intermediate certificates only
  • fullchain.pem --> All certificates. This includes the server certificate. It's a concatenation of cert.pem and chain.pem

When you generate new certificates the old ones will be stored in

/etc/letsencrypt/archive

To increase security I create a strong Deffie-Hellman group by running the following command

sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048

This will generate a file called dhparam.pem in

/etc/ssl/certs/

We're now ready to configure Nginx with our certificates.

Configuring Ngnix to use our SSL certificates

Now that we've obtained our SSL certifications, we need to configure Nginx. We'll need to configure Ngnix to rewrite all non-ssl traffic to our SSL port, turn on ssl, point Ngnix to our obtained certificates and configure the server to only allow the most secure ssl protocols and ciphers. Finally we'll setup Nginx to use the Deffie-Hellman group file we generated.

First lets create a server block to rewrite all insecure traffic to our secure port. We want to create a new server block above the default server block that looks like this.

server {
    listen      80;
    server_name {{ nginx_server_name }};
    rewrite     ^ https://$server_name$request_uri? permanent;
}

As you can see I set my nginx_server_name as a variable in my ansible vars file. This way I can make changes to the server name in one place and the rest of the ansible files will be updated. Now that we have the server rewriting all traffic over SSL, we need to turn on ssl and tell Nginx where to read our certificates from. In the main server block we are going to add the following lines

listen              443;
server_name         {{ nginx_server_name }};
ssl on;
ssl_certificate     {{ ssl_cert_dir }};
ssl_certificate_key {{ ssl_cert_key }};

Again I am referencing nginx_server_name, ssl_cert_dir and ssl_cert_key as ansible variables that I've configured in my ansible vars file. Now that we have turned on SSL and are listening on port 443 we need to setup our server to only allow the most secure protocols and ciphers. Below the ssl_certificate_key you can add the below configuration options.

# Allow only the most secure SSL protocols and ciphers, and use strong Diffie-Hellman.
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_dhparam /etc/ssl/certs/dhparam.pem;
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: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';
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_stapling on;
ssl_stapling_verify on;
add_header Strict-Transport-Security max-age=15768000;

Now we are setup to run Ngnix over SSL. You can save, exit and reload Nginx at this point.

sudo /etc/init.d/nginx reload

Inorder to make the server configuration reusable I usually build my Ngnix config as a template in my ansible playbooks using Jinja2. This allows me to easily deploy and configure Ngnix on any new servers that I'm going to deploy. This also allows me to automate the setup and configure of Ngnix. This way I only have to generate the Let's Encrypt certificates and then I can set the rest up using Ansible files.

Below is a sample Nginx configuration file.

upstream {{ application_name }}_wsgi_server {
     # fail_timeout=0 means we always retry an upstream even if it failed
     # to return a good HTTP response (in case the Unicorn master nukes a
     # single worker for timing out
server unix:{{ virtualenv_current }}/run/gunicorn.sock fail_timeout=0;
}
server {
    listen      80;
    server_name {{ nginx_server_name }};
    rewrite     ^ https://$server_name$request_uri? permanent;
}
server {
    listen              443;
    server_name         {{ nginx_server_name }};
    ssl on;
    ssl_certificate     {{ ssl_cert_dir }};
    ssl_certificate_key {{ ssl_cert_key }};
    # Allow only the most secure SSL protocols and ciphers, and use strong Diffie-Hellman.
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;
    ssl_dhparam /etc/ssl/certs/dhparam.pem;
    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: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';
   ssl_session_timeout 1d;
   ssl_session_cache shared:SSL:50m;
   ssl_stapling on;
   ssl_stapling_verify on;
   add_header Strict-Transport-Security max-age=15768000;
   # Letsencrypt auto-renewal configuration
   location ~ /.well-known {
       allow all;
   }
  client_max_body_size 4G;
  gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml
      application/xml+rss text/javascript image/svg+xml application/vnd.ms-fontobject;
  access_log {{ nginx_access_log_file }};
  error_log {{ nginx_error_log_file }};
  location /static/ {
     alias   {{ nginx_static_dir }};
  }
  location /media/ {
      alias   {{ nginx_media_dir }};
  }
    location / {
        if (-f {{ virtualenv_current }}/maintenance_on.html) {
            return 503;
        }
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header Host $http_host;
        proxy_redirect off;
        # Try to serve static files from nginx, no point in making an
        # *application* server like Unicorn/Rainbows! serve static files.
        if (!-f $request_filename) {
            proxy_pass http://{{ application_name }}_wsgi_server;
            break;
        }
    }
    # Error pages
    error_page 500 502 504 /500.html;
    location = /500.html {
        root {{ web_app_path }}/{{ application_name }}/templates/;
     }
     error_page 503 /maintenance_on.html;
     location = /maintenance_on.html {
         root {{ virtualenv_current }}/;
     }
}
### DOMAIN REDIRECTS ###
# Redirect non-www top-level domain to www
server {
       listen 80;
       listen 443;
       server_name  {{ site_url }};
      return 301 https://{{ site_url }}$request_uri;
}

Auto Renewal

Let's Encrypt certificates are valid for only 90 days. They recommend updating the certificate every 60 days though. This allows you to catch any errors that may occur before the certificate expires. Since you can obtain a new certificate from the Let's Encrypt client we are going to set up the renew process on a cron job. This will ensure we don't have to think about regenerating a new certificate every 90 days. I've set this up by creating a cron job in one of my ansible files.

---
# Setup Let's Encrypt auto-renew feature.  The cron job will run every Monday
# at 2:30am.  Then at 2:40am we will reload the nginx server.
- name: Lets Encrypt auto renew.
  cron: name="letsencrypt-auto renew"
      hour=2
      minute=30
      weekday=1
      user={{ gunicorn_user }}
      job="/opt/letsencrypt/letsencrypt-auto renew >> /var/log/le-renew.log"
      state=present

- name: Ngnix reload for Lets Encrypt
  cron: name="nginx-reload"
      hour=2
      minute=40
      weekday=1
      user={{ gunicorn_user }}
      job="/etc/init.d/nginx reload"
      state=present

The cron I setup is very naive and simple. I'm calling the Let's Encrypt client with the renew command

/opt/letsencrypt/letsencrypt-auto renew

I'm piping the output to a log file to help with debugging. The cron is set to run every Monday at 2:30am. The second cron job will reload Nginx every Monday at 2:40am to ensure the new certificate is loaded if one is found. That's it, I'm now able to renew my certificate every 60 days.

Final Thoughts

In the past purchasing and getting a certificate has always been a pain and it's expensive. Let's Encrypt now gives you an option that will save you both time and money. I've always dreaded the day I had to sit down and spend an hour getting the certificate setup. It also killed me knowing I had to spend money on it when it should just be free. Now I'm able to set everything up once and focus on the important parts of my projects.

References

This entire process of setting up Let's Encrypt is based on this amazing Digital Ocean article.

For more details on what's going on with Let's Encrypt you can look at their documentation.

Tags: