Edit July 22, 2016: Edited as the original letsencrypt-auto client is no longer supported. (has been replaced by: certbot-auto)


On December 3rd 2015 the EFF (Electronic Frontier Foundation) launched the LetsEncrypt public beta. As soon as I read about this initiative I have kept an eye on it. As the certificate for one of my web applications was about to expire it was the perfect time to dive into it.

What Lets Encrypt (LE) is:

Vital personal and business information flows over the Internet more frequently than ever, and we don’t always know when it’s happening. It’s clear at this point that encrypting is something all of us should be doing. Then why don’t we use TLS (the successor to SSL) everywhere? Every browser in every device supports it. Every server in every data center supports it. Why don’t we just flip the switch?...

Let’s Encrypt is a new free certificate authority, built on a foundation of cooperation and openness, that lets everyone be up and running with basic server certificates for their domains through a simple one-click process...

The key principles behind Let’s Encrypt are:

  • Free: Anyone who owns a domain can get a certificate validated for that domain at zero cost.

  • Automatic: The entire enrollment process for certificates occurs painlessly during the server’s native installation or configuration process, while renewal occurs automatically in the background.

  • Secure: Let’s Encrypt will serve as a platform for implementing modern security techniques and best practices.

  • Transparent: All records of certificate issuance and revocation will be available to anyone who wishes to inspect them.

  • Open: The automated issuance and renewal protocol will be an open standard and as much of the software as possible will be open source.

  • Cooperative: Much like the underlying Internet protocols themselves, Let’s Encrypt is a joint effort to benefit the entire community, beyond the control of any one organization. Source


Assumptions

This tutorial assumes you have knowledge of the previous blog post on Pound load balancing as we will be using the same architecture here. (Ubuntu 14.04)

LE Client

LE offers a client for requesting, renewing and revoking certificates. It uses the ACME protocol for domain validation. In short it tells the Web server to serve a specific page which the LE server will verify. Please refer to LE How It Works for more details.

The LE client offers three ways (called plugins) to serve the mentioned webpage:

  • Apache
    • Automates both obtaining and installing certs on an Apache web server.
  • Nginx
    • Same as Apache but under development
  • Standalone
    • LE's own web server which will bind to port 8o or 443
  • Webroot
    • Creates a temporary file in ${webroot-path}/.well-known/acme-challenge
  • Manual
    • Step by step process via the command line

Obstacles

As I am using Pound as a reverse proxy and TLS offloader/Accelarator (Check out my first blog on Pound) I will either need to use the Standalone or Manual method to request/renew LE certificates. This is required as the private key and certificates need to be stored on the Pound node. Moving the files from the back end web server over to the Pound every time they need to be renewed (LE certificates are valid for 90 days) isn't very sleek and I prefer not moving around the private key at all. Additionally I would like to automate the process making standalone the only valid option. This does however result in two challenges:

  1. Port 80 and 443 are used by Pound
  2. I distinguish request based on the hostname in the header (see here) which would send LE's traffic to the back-end (which isn't actually hosting the required page)

We will solve this obstacles later on, first we will install the LE client on the same node as Pound.

Installing LE Client

  1. Lets start with getting the system up to date
    sudo apt-get update && sudo apt-get upgrade -y && sudo apt-get dist-upgrade -y && sudo apt-get autoremove -y

  2. Create a folder to store the client in
    sudo mkdir /etc/letsencrypt

  3. cd to the newly created folder
    cd /etc/letsencrypt

  4. Download the client and mark it executable
    wget https://dl.eff.org/certbot-auto && sudo chmod a+x certbot-auto

That's all there is to it let's continue with configuring Pound

Overcoming the obstacles

Now we will overcome the two obstacles identified before (port usage and distinguishing LE traffic)

The LE client offers the option (--http-01-port) to listen on a different port than 80 or 443 which allows us to keep Pound running while requesting or renewing certs. We will use this option later on when requesting and renewing certificates to run the client on port 8000.

The LE server will however still try to reach the web page (used for domain validation) on port 80. We will need some magic to redirect LE's traffic to the pound node's port 8000 while still forwarding the other request to the correct back ends.

I got this idea from Thomas from the LE Community who did this trick using Apache modproxy and modproxy_http. He differentiates the request based on the URL. The LE client will serve the web page using the following URL "/.well-known/acme-challenge/.*" this will we use to forward the LE traffic to the localhost port 8000 where the standalone server is running.

The image below shows how normal traffic on port 80 is redirect to either back end server while URL matching the ACME protocol will be redirected to port 8000 on the localhost.

Architecture

To achieve this we will define a new service in the Pound config which will match on the URL.

  1. Add the following service to the Pound config
ListenHTTP  
Service  
    URL "/.well-known/acme-challenge/.*"
    BackEnd
        Address 127.0.0.1
        Port 8000
    End
End  
  1. Check the config file
    sudo pound -c
    Output: Config file /usr/local/etc/pound.cfg is OK

  2. Restart Pound to load new config
    sudo /etc/init.d/pound restart

  3. To request a new certificate the following command should be used
    sudo /etc/letsencrypt/certbot-auto certonly --standalone --agree-tos --domains [domain_name] --email [e-mail address] --standalone-supported-challenges http-01 --http-01-port 8000

    • certonly
      Obtains a cert, but does not install it
    • --standalone
      Run a standalone web server for authentication
    • --agree-tos
      Agree to LE terms of services
    • --email [e-mail address]
      E-mail to register with
    • --domains [domain_name]
      Domain or domains the certificate will be valid for
    • --standalone-supported-challenges http-01
      Use port 80 for domain validation
    • --http-01-port 8000
      Override the port to 8000

Automation and Pound

As the certificates are only valid for 90 days it is of added value to automate the renewal process. I created a bash script that will request or renew certificates and additionally create the certificate file in Pound's format. The script will check the number of days the certificate is still valid for and renew if equal or less than 30 days.

  1. Grab the script
    sudo curl https://raw.githubusercontent.com/secwiseblog/Lets-Encrypt/master/LE_Pound_Request_Renew.sh > /tmp/LE_Pound_Request_Renew.sh

  2. Move the script to the /etc/letsencrypt folder and change permissions
    sudo mv /tmp/LE_Pound_Request_Renew.sh /etc/letsencrypt/LE_Pound_Request_Renew.sh sudo chown root:root /etc/letsencrypt/LE_Pound_Request_Renew.sh
    sudo chmod +x /etc/letsencrypt/LE_Pound_Request_Renew.sh

  3. Edit the parameters up until the divider

  4. Add the script as a cron job - running daily (Thanks to Brendan M. Sleight for noting the fact that .sh script do not run in /etc/cron.daily)
    sudo ln -s /etc/letsencrypt/LE_Pound_Request_Renew.sh /etc/cron.daily/LE_Pound_Request_Renew

LE_Pound_Request_Renew.sh

Note: 2 additions by Kent Hinson

  1. On debian change #!/bin/sh to #!/bin/bash
  2. Added --rsa-key-size 4096 to the cert request to force RSA 4096 (required for A+ score on SSL Labs - At the time of writing still overkill though)
#!/bin/sh
#! /bin/sh
# -----------------------------------------------------------------
# v1.0
# This script will use Lets Encrypt (LE) to request and/or renew certificates automatically.
# The script will also concatenate the private key and cert chain (Pound's format) and make it available to Pound Load Balancer
# v1.1
# switch to certbot-auto
# -----------------------------------------------------------------

# Parameters
# -----------------------------------------------------------------
# domains to request certs for, separated by a space
domains=(one.example.com two.example.com))  
# email address to user when registering certs
email=postmaster@example.com  
# LE Binary folder (LE binaries)
le_bin=/etc/letsencrypt  
# LE Output Folder (default /etc/letsencrypt)
le_output=/etc/letsencrypt  
# Port to bind LE standalone server to
le_port=8000  
# Pound folder
pound_fol=/etc/pound  
# Pound Cert folder
pound_cfol=${pound_fol}/certs  
# -----------------------------------------------------------------
# --------------- Do not edit beyond this point -------------------

# Functions
# -----------------------------------------------------------------
# function extracts the number of days the cert in question is still valid for
# Original work by Acetylator (https://community.letsencrypt.org/t/how-to-completely-automating-certificate-renewals-on-debian/5615)
get_days_exp() {  
    echo "grep the number of days the cert if valid for"
    local d1=$(date -d "`openssl x509 -in $1 -text -noout|grep "Not After"|cut -c 25-`" +%s)
    local d2=$(date -d "now" +%s)

    echo "Return result in global variable"
    days_exp=$(echo \( $d1 - $d2 \) / 86400 |bc)
}

# Function to create certificate is pound's required format
create_pound_cert() {  
    echo "Create a PEM file in Pound's format / Combine the private key with fullchain"
    cat ${1} > ${3}
    cat ${2} >> ${3}

    echo "Fix owner and permissions for ${3}"
    chown www-data:www-data ${3}
    chmod 644 ${3}
}
#-----------------------------------------------------------------

# Execution
# -----------------------------------------------------------------
# Create Pound certs folder if it does not exists yet
# Make sure that the cert paths point to the correct folder in the Pound config file
if [ ! -d ${pound_cfol} ]; then  
    echo "creating ${pound_cfol}"
    mkdir ${pound_cfol}

    echo "fix owner and permissions for ${pound_cfol}"
    chown www-data:www-data ${pound_cfol}
fi

echo "For each domain in '$domains' array check certs"  
for domain_name in "${domains[@]}"  
    do
        # Variables for this for loop (Required as it used the domain_name from the domains array)
        # ---------------------------
        # LE Live folder
        le_live=${le_output}/live/${domain_name}
        # LE live certs
        le_cert=${le_live}/cert.pem
        # Pound cert folder for every domain
        pound_cert=${pound_cfol}/${domain_name}
        # ---------------------------

        # if a Pound cert file does not exist
        echo "Checking if ${pound_cert} exists"
        if [ ! -e ${pound_cert}  ]; then
            echo "${pound_cert} does not exist"

            # if a LE cert does not exist request it
            echo "Checking if ${le_cert} exists"
            if [ ! -e ${le_cert} ]; then
                echo "${le_cert} does not exist"
                echo "Requesting cert for ${domain_name}"
                ${le_bin}/certbot-auto certonly --standalone --agree-tos --domains ${domain_name} --email ${email} --standalone-supported-challenges http-01 --http-01-port 8000 --renew-by-default --rsa-key-size 4096
            fi

            echo "Creating pound cert for ${domain_name}"
            create_pound_cert ${le_live}/privkey.pem ${le_live}/fullchain.pem ${pound_cert}

            echo "set parameter used to determine if pound needs to be restarted"
            restart=1
        fi

        echo "Check the number of days the cert is still valid for"
        get_days_exp "${le_cert}"
        echo "${domain_name}'s cert is valid for another ${days_exp}"

        # If the certificate is valid for 30 or less days
        if [ ${days_exp} -le "30" ]; then
            # The renew command is the same as the initial request command - it will use the config file in ${le_output}/renewal
            # if you used LE for this domain before (for example using the test parameter) you may need to alter the renew config file
            echo "Renewing cert for ${domain_name}"
            ${le_bin}/certbot-auto certonly --standalone --agree-tos --domains ${domain_name} --email ${email} --standalone-supported-challenges http-01 --http-01-port 8000 --renew-by-default --rsa-key-size 4096

            echo "Creating pound cert for ${domain_name}"
            create_pound_cert ${le_live}/privkey.pem ${le_live}/fullchain.pem ${pound_cert}

            echo "set parameter used to determine if pound needs to be restarted"
            restart=1
        fi
    done

if [ "${restart}" == "1" ]; then  
    echo "Restart Pound to load new certs"
    /etc/init.d/pound restart
else  
    echo "No new or renewed certs - no restart required"
fi  
# -----------------------------------------------------------------

Disclaimer

I have only touched a couple of LE's features, I am not a subject matter expert, but you are free to ask me anything.

Like this post? Please consider leaving a comment; it's what keeps me going