Using centralised management with Lets Encrypt

 

Lets Encrypt is a service that provides free DV SSL Certificates. Since StartSSL had issues and are being delisted, I needed an alternative.

The one thing that put me off Lets Encrypt for so long is that I could no longer administer all my certs from a central location. This meant running software on several systems to keep the certs updated – or manual intervention every 90 days. This wasn’t acceptable to me – so I hunted for another solution.

Enter the DNS-01 validation challenge. The Lets Encrypt recommended solution (certbot) doesn’t support the DNS-01 method yet – so I needed something else.

This solution is aimed at those who have your own domain, and have many hosts that you use certs on (could be anything from SQL servers, to mail, to web servers) and want to manage them all from the same place – and not run additional software on them.

In this guide, I’ve used ‘example.com‘ as the domain name. Adjust to suit.

  1. Set up your VM to use for the cert management. As always, I used Scientific Linux 7. You can use whatever you like, just adjust accordingly.
  2. Install bind, then create your base zone file in /var/named/data/ for ‘le.example.com‘ with something like:
    $ORIGIN .
    $TTL 21600      ; 6 hours
    le.example.com          IN SOA  certvm.example.com. webmaster.example.com. (
                                    2016112648 ; serial
                                    43200      ; refresh (12 hours)
                                    3600       ; retry (1 hour)
                                    1209600    ; expire (2 weeks)
                                    3600       ; minimum (1 hour)
                                    )
                            NS      certvm.example.com.
    
  3. Set up bind and dynamic DNS updates with ‘nsupdate’. There is lots written on this topic that is covered in much more detail that I could cram into here. I like the guide nsupdate: Painless Dynamic DNS.
  4. Now we set up the LE software. I used ‘dehydrated‘ (previously letsencrypt.sh). You’ll want to check this out as a non-root user.
    # git clone https://github.com/lukas2511/dehydrated.git
  5. I wrote a hook file that gets called from dehydrated while it goes through its process. Save it to the directory you cloned dehydrated into under the filename ‘dns-hook.sh
    #!/usr/bin/env bash
    
    #
    # Example how to deploy a DNS challenge using nsupdate
    #
    
    set -e
    set -u
    set -o pipefail
    
    NSUPDATE="/usr/bin/nsupdate -k /home/user/Knsupdate-key.+157+17276.key"
    TTL=300
    
    case "$1" in
        "deploy_challenge")
            $NSUPDATE <<-EOF
                    server 127.0.0.1
                    zone le.example.com.
                    update add ${2}.le.example.com. ${TTL} in TXT "${4}"
                    send
            EOF
            ;;
        "clean_challenge")
            $NSUPDATE <<-EOF
                    server 127.0.0.1
                    zone le.example.com.
                    update delete ${2}.le.example.com. ${TTL} in TXT "${4}"
                    send
            EOF
            ;;
        "deploy_cert")
            /home/user/bin/deploy_cert "${1}" "${2}" "${3}" "${4}"
            ;;
        "unchanged_cert")
            # do nothing for now
            ;;
        *)
            echo Unknown hook "${1}"
            exit 1
            ;;
    esac
    
    exit 0
  6. Configure dehydrated by coping the example config file in docs/examples, and set:
    CA="https://acme-staging.api.letsencrypt.org/directory"
    CHALLENGETYPE="dns-01"
    HOOK=${BASEDIR}/dns-hook.sh

    Once you have verified that all these steps work, you can comment out the CA line to obtain live certs.

  7. Now we create the deployment script called from dns-hook.sh as ‘~/bin/deploy_cert‘:
    #!/bin/bash
    set -e
    
    HOST=$2
    SSHOPTIONS="-o ControlMaster=auto -o ControlPersist=60 -o ControlPath=~/.ssh/%r@%h-%p"
    
    ## Do we have a config file for this host?
    if [ -f $HOME/deployment/$2 ]; then
            . $HOME/deployment/$2
    else
            echo "No config file for host $host"
            echo "Aborting..."
            exit 0;
    fi
    
    echo "Starting deployment to $HOST"
    
    ## Connect to the remote server and leave session open for max 60 seconds.
    ssh $HOST $SSHOPTIONS "/bin/true"
    
    ## Do we have a cert to deploy?
    if [ -f "$HOME/dehydrated/certs/$2/cert.pem" ] && [ ! -z "$CERT" ]; then
            echo "$HOST - Copying CERT to $CERT"
            CERT_FILE=`cat "$HOME/dehydrated/certs/$2/cert.pem"`
            ssh $HOST $SSHOPTIONS \
    "cat << 'EOF' > $CERT
    $CERT_FILE
    EOF"
    fi
    
    ## Do we have a key to deploy?
    if [ -f "$HOME/dehydrated/certs/$2/privkey.pem" ] && [ ! -z "$KEY" ]; then
            echo "$HOST - Copying KEY to $KEY"
            KEY_FILE=`cat "$HOME/dehydrated/certs/$2/privkey.pem"`
            ssh $HOST $SSHOPTIONS \
    "cat << 'EOF' > $KEY
    $KEY_FILE
    EOF"
    fi
    
    ## Do we have a chain to deploy?
    if [ -f "$HOME/dehydrated/certs/$2/chain.pem" ] && [ ! -z "$CHAIN" ]; then
            echo "$HOST - Copying CHAIN to $CHAIN"
            CHAIN_FILE=`cat "$HOME/dehydrated/certs/$2/chain.pem"`
            ssh $HOST $SSHOPTIONS \
    "cat << 'EOF' > $CHAIN
    $CHAIN_FILE
    EOF"
    fi
    
    ## Do we have a fullchain to deploy?
    if [ -f "$HOME/dehydrated/certs/$2/fullchain.pem" ] && [ ! -z "$FULLCHAIN" ]; then
            echo "$HOST - Copying FULLCHAIN to $FULLCHAIN"
            FULLCHAIN_FILE=`cat "$HOME/dehydrated/certs/$2/fullchain.pem"`
            ssh $HOST $SSHOPTIONS \
    "cat << 'EOF' > $FULLCHAIN
    $FULLCHAIN_FILE
    EOF"
    fi
    
    ## Do we have a command to run afterwards?
    if [ ! -z "$COMMAND" ]; then
            echo "$HOST - Executing remote command: $COMMAND"
            ssh -t $HOST $SSHOPTIONS "$COMMAND"
    fi
    
  8. Create the directory $HOME/deployment and create a text file (for example) www.example.com:
    HOST=webhost.example.com
    CERT=/path/to/www.example.com.crt
    KEY=/path/to/www.example.com.key
    CHAIN=/path/to/intermediate.crt
    COMMAND="sudo service httpd reload"
    

    Within this file CERT, KEY and CHAIN are the files that hold your SSL cert on the server. HOST is the SSH server that hosts your web site. COMMAND is the command run after the cert is copied across. It is expected that you be able to set up SSH keys to do this without entering a password to allow automatic updates of your certs.

Now we turn our attention to the main DNS server that serves your domain to the public.

  1. In your main DNS server for example.com, we now want to delegate the entire ‘le.example.com’ namespace to the DNS server we just created. Don’t forget to also set an IP for the certvm system. This will look something like:
    certvm        IN        A        1.2.3.4
    le            IN        NS       certvm.example.com.
  2. Next, for each fqdn we want to create a certificate for, we want to create a CNAME back to the le.example.com namespace – for example:
    _acme-challenge.www        IN        CNAME www.example.com.le.example.com.
    _acme-challenge.sslserver  IN        CNAME sslserver.example.com.le.example.com.

You should now be ready to run dehydrated for the first time.

After verifying that your setup works, remember to remove the CA line from the dehydrated config file.

Feel free to leave feedback on this guide – I wrote it up mostly from memory – so I may well have missed a step.

Changes:
* Dec 2016: Updated dns-hook script to use EOF instead of printf – suggested by TCM @ Lets Encrypt Community

  3 Responses to “Using centralised management with Lets Encrypt”

  1. Thanks for this great write-up! I was going to make an _acme-challenge subdomain for each subdomain to be certified (with NS and A records for each) but I like your CNAME approach better.

    Thanks for sharing!

    (Blog Format Comment: please put the date in your articles. It makes it difficult to know if it the info is outdated or not if its missing šŸ™‚

  2. Hmmm i am struggling to get this all working and keep getting the below error

    c0mputerking@certs:~$ dehydrated/dehydrated –cron –domain cloud.solar.fake.com
    # INFO: Using main config file /home/c0mputerking/dehydrated/config
    Processing cloud.solar.fake.com
    + Signing domains…
    + Generating private key…
    + Generating signing request…
    + Requesting challenge for cloud.solar.fake.com…
    + Responding to challenge for cloud.solar.fake.com…
    Unknown hook invalid_challenge

    Not much to go on i know but i do get some stuff about updates in my syslog about bind being updated so some things are happening

    Jun 14 15:03:44 certs named[958]: client 127.0.0.1#22157/key nsupdate-key: signer “computerking-ca” approved
    Jun 14 15:03:44 certs named[958]: client 127.0.0.1#22157/key nsupdate-key: updating zone ‘le.computerking.ca/IN’: adding an RR at ‘cloud.solar.fake.com.le.fake.com’ TXT
    Jun 14 15:04:07 certs named[958]: client 127.0.0.1#3135/key nsupdate-key: signer “computerking-ca” approved
    Jun 14 15:04:07 certs named[958]: client 127.0.0.1#3135/key nsupdate-key: updating zone ‘le.computerking.ca/IN’: deleting an RR at cloud.solar.fake.com.le.fake.com TXT

 Leave a Reply

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

(required)

(required)