Post

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.

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.

Install bind, then create your base zone file in /var/named/data/ for ‘le.example.com’ with something like:

1
2
3
4
5
6
7
8
9
10
$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.

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.

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.

1
# git clone https://github.com/lukas2511/dehydrated.git

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#!/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}"
	;;
    *)
	echo "Nothing to do for hook ${1}"
	exit 0
	;;
esac

exit 0

Configure dehydrated by coping the example config file in docs/examples, and set:

1
2
3
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.

Now we create the deployment script called from dns-hook.sh as ~/bin/deploy_cert:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#!/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

Create the directory $HOME/deployment and create a text file (for example) www.example.com:

1
2
3
4
5
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.

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:

1
2
certvm        IN        A        1.2.3.4
le            IN        NS       certvm.example.com.

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:

1
2
_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

  • Jan 2019: Updated dns-hook script to not exit with code 1 on unknown hooks. Just print that there’s nothing to do instead.

This post is licensed under CC BY 4.0 by the author.