Letsencrypt using DNS verification with CFEngine
To use this bundle, you will need to set up dynamic DNS, for instance as I explained in https://haavard.name/2016/03/15/setting-up-key-based-dynamic-dns-updates-with-cfengine/.
I’m using a file repository in $(def.dir_files)
where you will need to download letsencrypt.sh in $(def.dir\_files)/usr/local/bin
. You also need to setup config.sh
in $(def.dir\_files)/etc/letsencrypt.sh/config.sh
. Use the default config.sh
and modify as you see fit, the important settings is
CHALLENGETYPE="dns-01"
BASEDIR=/etc/letsencrypt.sh
HOOK=/etc/letsencrypt.sh/hook-dns.sh
Then install this hook script for updating dns when needed to verify ownership
#!/bin/bash
function deploy_challenge {
local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}"
# This hook is called once for every domain that needs to be
# validated, including any alternative names you may have listed.
#
# Parameters:
# - DOMAIN
# The domain name (CN or subject alternative name) being
# validated.
# - TOKEN_FILENAME
# The name of the file containing the token to be served for HTTP
# validation. Should be served by your web server as
# /.well-known/acme-challenge/${TOKEN_FILENAME}.
# - TOKEN_VALUE
# The token value that needs to be served for validation. For DNS
# validation, this is what you want to put in the _acme-challenge
# TXT record. For HTTP validation it is the value that is expected
# be found in the $TOKEN_FILENAME file.
modify_key $DOMAIN
find_master_and_zone "$DOMAIN"
cat <<EOD | nsupdate -k "$key"
server ${MASTER}
zone ${ZONE}
update delete _acme-challenge.${DOMAIN}.
update add _acme-challenge.${DOMAIN}. 60 IN TXT ${TOKEN_VALUE}
send
EOD
#add sleep to allow update to propagate out to all slaves and such
sleep 61
}
function clean_challenge {
local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}"
# This hook is called after attempting to validate each domain,
# whether or not validation was successful. Here you can delete
# files or DNS records that are no longer needed.
#
# The parameters are the same as for deploy_challenge.
modify_key $DOMAIN
find_master_and_zone "$DOMAIN"
cat <<EOD | nsupdate -k "$key"
server ${MASTER}
zone ${ZONE}
update delete _acme-challenge.${DOMAIN}.
send
EOD
}
function deploy_cert {
local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" CHAINFILE="${4}"
# This hook is called once for each certificate that has been
# produced. Here you might, for instance, copy your new certificates
# to service-specific locations and reload the service.
#
# Parameters:
# - DOMAIN
# The primary domain name, i.e. the certificate common
# name (CN).
# - KEYFILE
# The path of the file containing the private key.
# - CERTFILE
# The path of the file containing the signed certificate.
# - CHAINFILE
# The path of the file containing the full certificate chain.
}
function modify_key {
local DOMAIN="${1}"
if ! egrep -q "^${DOMAIN}" "${key}"
then
newkey="${tmpdir}/$(basename ${key})"
sed -r "s/^[^ ]+/${DOMAIN}./" "${key}" > "${newkey}"
cp /etc/ssl/private/dnskeys/"$(basename ${key} .key)".private "${tmpdir}/"
key="${newkey}"
fi
}
function find_master_and_zone {
local name=${1}
while [ "x$name" != "x" ]
do
soa=$(dig +short soa $name)
if [ "x$soa" != "x" ]
then
MASTER=${soa/. *}
ZONE=$name
break
fi
name=${name#*.}
done
}
key=$(find /etc/ssl/private/dnskeys -name '*key' | head -1)
if [ ! -f "$key" ]
then
exit 1
fi
tmpdir=$(mktemp -d) || exit 1
HANDLER=$1; shift; $HANDLER $@
rm -rf -- "$tmpdir"
Then finally the CFEngine bundle
vars:
any::
"letsencrypt_config_dir" string => "/etc/letsencrypt.sh";
"letsencrypt_domains_text" string => "$(letsencrypt_config_dir)/domains.txt";
"letsencrypt_binary" string => "/usr/local/bin/letsencrypt.sh";
server1::
"domains" slist => { "server1.example.com" };
server2::
"domains" slist => { "server2.example.com www.example.com", "foo.example.com" };
classes:
Q1::
"has_domains" expression => isvariable("domains");
files:
has_domains::
"$(letsencrypt_config_dir)"
copy_from => no_backup_dcp("$(def.dir_files)$(letsencrypt_config_dir)"),
depth_search => recurse("1");
"$(letsencrypt_config_dir)/certs/."
depth_search => recurse_with_base("3"),
file_select => dirs,
perms => mog("0710", "root", "ssl-cert");
"$(letsencrypt_config_dir)/certs/."
depth_search => recurse("3"),
file_select => plain,
perms => mog("0640", "root", "ssl-cert");
"$(letsencrypt_config_dir)/hook.*"
perms => mog("0755", "root", "root");
"$(letsencrypt_domains_text)"
create => "true",
perms => mog("0640", "root", "ssl-cert"),
edit_line => converge(".*", "@(domains)");
"$(letsencrypt_binary)"
copy_from => no_backup_dcp("$(def.dir_files)$(letsencrypt_binary)"),
perms => mog("0755", "root", "root");
commands:
Hr01.Q2::
"$(letsencrypt_binary) --cron >/dev/null"
contain => in_shell;
}
You set up the list of domains in the vars section, if you have an entry with several names seperated by space, you will get a SAN certificate. Remember that to insert the KEY record for all entries using the private dns key of the server.