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.com
ns2.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 includemaster
andslave
roles 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.com
we should provide sameNS
records as we have inexample.com
zone or Let’s Encrypt API may complain.
This configuration should be enough for knsupdate
to work. Will check in next step.
knsupdate
Dynamic DNS updates with You could find knsupdate
manual here.
Let’s setup some env variables which we will use in code snippets below:
Pay attention to
KNOT_KEY
it 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:
cleanup
auth
(default, implicitly callscleanup
before execution)
Subcommand
auth
will 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
.sh
should havex
chmod, so don’t forget tochmod +x
them
We are ready to run Certbot with this hooks:
Will be using staging certificate authority (
--test-cert
option)
$ 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 ...