Using Certbot with Knot DNS (knsupdate)
This is a note about the integration of the Certbot with Let’s encrypt DNS–01 authentication mechanism. Will not use any cloud services (fuck cloud), just self-hosted DNS instances, like good old times. I’ll show how to configure Knot DNS to accept dynamic DNS updates from knsupdate and how to create a rudimentary hook for Certbot which will use knsupdate to set TXT records with _acme-challenge.
Knot-specific configuration
Say we have two DNS servers:
ns1.example.comns2.example.com
We need a key which will be used to sign our dynamic DNS updates. Here is how to generate it with keymgr (which is part of Knot):
$ keymgr tsig generate -t dyndns hmac-sha512 | tee /secrets/dyndns # hmac-sha512:dyndns:Me2/vFaNQp7dNXeNt3NuMgRWujxO5g1HwyV8nmn1YerdulKhMoFe8UjBvEfFVTYJmBywSoX4VIDzuaCdlfTbXA== key: - id: dyndns algorithm: hmac-sha512 secret: Me2/vFaNQp7dNXeNt3NuMgRWujxO5g1HwyV8nmn1YerdulKhMoFe8UjBvEfFVTYJmBywSoX4VIDzuaCdlfTbXA=
For our example we saving this into /secrets/dyndns and will include this secret in configuration. Field with label id is mandatory and will be used to reference this secret from configuration.
Here is part of Knot configuration:
I assume you have Knot configured and running. If not then this is out of scope for this article. See Knot documentation.
include: /secrets/dyndns acl: - id: dyndns key: dyndns action: update ... template: - id: main acl: [ dyndns ] ... ... zone: - domain: _acme-challenge.example.com template: main file: /data/_acme-challenge.example.com.zone
We define ACL which references previously generated key, then we reference ACL in template which will be used to control updates for _acme-challenge.example.com zone.
Why separate zone for
_acme-challenge?
Because my configuration does not includemasterandslaveroles so we have no notifications between servers, I have reasons to do this to mitigate possible zone serial collisions which may break whole zone (my zones are static read-only files provisioned with configuration managements system)
Now let’s describe /data/_acme-challenge.example.com.zone file:
$TTL 86400 _acme-challenge.example.com. 10 IN SOA ns1.example.com. root.example.com. (2022021700 86400 600 864000 60) _acme-challenge.example.com. IN NS ns1.example.com. _acme-challenge.example.com. IN NS ns2.example.com.
For any subdomains like
service.example.comwe should provide sameNSrecords as we have inexample.comzone or Let’s Encrypt API may complain.
This configuration should be enough for knsupdate to work. Will check in next step.
Dynamic DNS updates with knsupdate
You could find knsupdate manual here.
Let’s setup some env variables which we will use in code snippets below:
Pay attention to
KNOT_KEYit will have value:
hmac-sha512:dyndns:Me2/vFaNQp7dNXeNt3NuMgRWujxO5g1HwyV8nmn1YerdulKhMoFe8UjBvEfFVTYJmBywSoX4VIDzuaCdlfTbXA==
export KNOT_SERVER=ns1.example.com export KNOT_KEY=$(cat /secrets/dyndns | head -n 1 | sed 's/^#\s*//g') export KNOT_ZONE=_acme-challenge.example.com export KNOT_SUBJECT=example.com export KNOT_SUBJECT_VALUE=test-txt-record-value
Set TXT record value with TTL = 60:
$ cat <<EOF | knsupdate server ${KNOT_SERVER} key ${KNOT_KEY} zone ${KNOT_ZONE}. update add _acme-challenge.${KNOT_SUBJECT}. 60 TXT "${KNOT_SUBJECT_VALUE}" send quit EOF
This should silently exit with 0 code. In case of error you will get some messages.
Now let’s request our TXT record directly from the nameserver:
$ host -t TXT ${KNOT_ZONE}. ${KNOT_SERVER} | grep ${KNOT_ZONE} _acme-challenge.example.com descriptive text "test-txt-record-value"
We have a value. Now let’s delete record:
$ cat <<EOF | knsupdate server ${KNOT_SERVER} key ${KNOT_KEY} zone ${KNOT_ZONE}. update del _acme-challenge.${KNOT_SUBJECT}. TXT send quit EOF
Make sure it is really deleted:
$ host -t TXT ${KNOT_ZONE}. ${KNOT_SERVER} | grep ${KNOT_ZONE} _acme-challenge.example.com has no TXT record
Good, we are ready to make Certbot hook.
Certbot hook
Certbot documentation about manual-auth-hook is here.
Also you can read about variables available inside hook here.
As we have do in previous paragraph, let’s define some variables which will be used in code snippets below:
export KNOT_SERVER=ns1.example.com export KNOT_KEY=$(cat /secrets/dyndns | head -n 1 | sed 's/^#\s*//g') export KNOT_ZONE=_acme-challenge.example.com export KNOT_SUBJECT=example.com
It is time to write our hook. It will use knsupdate to update records on each DNS server of the KNOT_ZONE.
This hook will accept two subcommands/actions:
cleanupauth(default, implicitly callscleanupbefore execution)
Subcommand
authwill be invoked if no subcommand has been provided.
Open certbot-knsupdate-hook.sh and write:
#! /usr/bin/env bash set -e set -o pipefail nameservers=$(host -t NS ${KNOT_ZONE}. ${KNOT_SERVER} \ | grep -F ' name server ' \ | awk -F ' name server ' '{print $2}') cleanup() { if [ ! -z "${CERTBOT_AUTH_OUTPUT}" ] then echo ${CERTBOT_AUTH_OUTPUT} fi for nameserver in ${nameservers} do cat <<EOF | knsupdate || true server ${nameserver} key ${KNOT_KEY} zone ${KNOT_ZONE}. update del _acme-challenge.${KNOT_SUBJECT}. TXT send quit EOF done } ## case "$1" in cleanup) cleanup ;; auth | *) cleanup trap cleanup ERR for nameserver in ${nameservers} do cat <<EOF | knsupdate server ${nameserver} key ${KNOT_KEY} zone ${KNOT_ZONE}. update add _acme-challenge.${KNOT_SUBJECT}. 60 TXT "${CERTBOT_VALIDATION}" send quit EOF done ;; esac
We have two subcommands, but certbot may only run scripts, it can not pass any arguments to this scripts. And we need to call cleanup after auth.
Because of this we need to create a separate wrapper for cleanup subcommand, open certbot-knsupdate-cleanup-hook.sh and write:
#! /usr/bin/env bash exec ./certbot-knsupdate-hook.sh cleanup
Of course all
.shshould havexchmod, so don’t forget tochmod +xthem
We are ready to run Certbot with this hooks:
Will be using staging certificate authority (
--test-certoption)
$ mkdir acme $ certbot certonly \ --manual -n -m root@example.com --agree-tos \ --test-cert --expand \ --config-dir=./acme \ --work-dir=./acme \ --logs-dir=./acme \ --preferred-challenges=dns \ --manual-auth-hook=./certbot-knsupdate-hook.sh \ --manual-cleanup-hook=./certbot-knsupdate-cleanup-hook.sh \ -d "*.${KNOT_SUBJECT}" -d "${KNOT_SUBJECT}"
This will issue a wildcard certificate for KNOT_SUBJECT and add non-wildcard hostname as alternative name into the certificate.
Certbot will output something similar to:
Saving debug log to /home/user/projects/corpix.dev/acme/letsencrypt.log Successfully received certificate. Certificate is saved at: /home/user/projects/corpix.dev/acme/live/example.com/fullchain.pem Key is saved at: /home/user/projects/corpix.dev/acme/live/example.com/privkey.pem This certificate expires on 2022-05-18. These files will be updated when the certificate renews. NEXT STEPS: - The certificate will need to be renewed before it expires. Certbot can automatically renew the certificate in the background, but you may need to take steps to enable that functionality. See https://certbot.org/renewal-setup for instructions.
If it complains see
./acme/letsencrypt.log, it should contain some information which helps.
Finally we could dump certificate information with openssl to make sure it contains expected data:
$ openssl x509 -noout -text -in ./acme/live/example.com/cert.pem Certificate: Data: ... Issuer: C = US, O = (STAGING) Let's Encrypt, CN = (STAGING) Artificial Apricot R3 Validity Not Before: Feb 17 15:06:00 2022 GMT Not After : May 18 15:06:00 2022 GMT Subject: CN = *.example.com ... Authority Information Access: OCSP - URI:http://stg-r3.o.lencr.org CA Issuers - URI:http://stg-r3.i.lencr.org/ X509v3 Subject Alternative Name: DNS:*.example.com, DNS:example.com ...