Benutzer-Werkzeuge

Webseiten-Werkzeuge


pr:le-dns-delegate

Unterschiede

Hier werden die Unterschiede zwischen zwei Versionen gezeigt.

Link zu der Vergleichsansicht

pr:le-dns-delegate [2023/05/03 22:25] (aktuell)
martok angelegt
Zeile 1: Zeile 1:
 +====== Delegating Let's Encrypt dns-01 to a dedicated zone ======
 +
 +===== Problem =====
 +
 +Let's Encryt offers [[https://​letsencrypt.org/​docs/​challenge-types/#​dns-01-challenge|ACME dns-01 challenges]]. Implementing them is straightforward,​ but has one drawback: since the zone is changed, the next time the zone file is written to disk, it is rewritten - and bind has //really weird// formatting.
 +
 +==== Concept ====
 +
 +Instead of responding to a challenge in individual zones, create one "​delegate zone" that receives the challenges via CNAME records. We don't care if that zone file gets trashed.
 +
 +For this example, we'll use the following setup:
 +
 +  * ''​ns1.example.com''​ - running bind9, and is the ''​NS''​ for the other domains. ''​ns2.example.com''​ is a secondary.
 +  * ''​client1.example''​ - a domain we want to create certs for
 +  * ''​client2.test''​ - another one
 +  * [[https://​github.com/​dehydrated-io/​dehydrated|dehydrate]] to manage certs
 +
 +==== Setup ====
 +
 +=== Domain Setup ===
 +
 +First, we need the zone that will handle all our ACME challenges. This is a regular [[https://​www.zytrax.com/​books/​dns/​ch9/​delegate.html|delegate domain]] (you may want to add appropriate DNSSEC to it):
 +
 +<file - acme.example.com.zone>​
 +$ORIGIN .
 +$TTL 3600       ; 1 hour
 +acme.example.com ​    IN SOA  ns1.example.com. admin.example.com. (
 +                                2023040100 ; serial
 +                                3600       ; refresh (1 hour)
 +                                3600       ; retry (1 hour)
 +                                86400      ; expire (1 day)
 +                                3600       ; minimum (1 hour)
 +                                )
 +                        NS      ns1.example.com.
 +                        NS      ns2.example.com.
 +                        MX      0 .
 +                        TXT     "​v=spf1 -all"
 +                        CAA     0 issue "​letsencrypt.org"​
 +                        CAA     0 issuewild "​letsencrypt.org"​
 +</​file>​
 +
 +Next, we'll need some TSIG keys used for the ''​nsupdate''​ calls.
 +
 +<file bash>
 +tsig-keygen -a hmac-sha512 client1.example.acme > Kexample.client1._acme-challenge.key
 +tsig-keygen -a hmac-sha512 client2.test.acme > Ktest.client2._acme-challenge.key
 +cat *._acme-challenge.key >> /​etc/​bind/​named.conf.keys
 +</​file>​
 +
 +Now we can use those to enable updates to the ACME delegate zone. The general pattern will be that each certificate target will be prepended as a subdomain to the delegate, so it becomes ''​${domain}.acme.example.com''​. We will create one of these entries for each client zone at the level where that zone's ''​SOA''​ record sits, //not// for its subdomains. This is just a convention to make things clearer/​self-documenting,​ no technical requirement.
 +
 +<file - acme.example.com.cfg>​
 +zone "​acme.example.com"​ IN {
 +        type master;
 +        file "/​etc/​bind/​zones/​acme.example.com.zone";​
 +
 +        update-policy {
 +                grant client1.example.acme. subdomain client1.example.acme.example.com. TXT;
 +                grant client2.test.acme. subdomain client2.test.acme.example.com. TXT;
 +        };
 +};
 +</​file>​
 +
 +On the side of the client domains, we need to tell ACME that it needs to use these zones for challenges. We do that by setting ''​CNAME''​ records in the client zones:
 +
 +<file - client1.example.zone>​
 +$ORIGIN client1.example.
 +_acme-challenge ​          ​CNAME ​  ​_acme-challenge.client1.example.acme.example.com.
 +$ORIGIN client2.example.
 +_acme-challenge ​          ​CNAME ​  ​_acme-challenge.client1.example.acme.example.com.
 +</​file>​
 +
 +Note that we point them all to the same location defined by the SOA+delegate-pattern explained above. In the second case, we even point a different domain to the same target. This could be used to share key material between multiple domains owned by the same entity.
 +
 +Next, we can tell dehydrated to use this setup for challenges.
 +
 +== Dehydrated Setup ==
 +
 +The ''​dns-01''​ challenge is handled by a hook script [[https://​github.com/​dehydrated-io/​dehydrated/​wiki/​example-dns-01-nsupdate-script|based on this one]] which uses ''​nsupdate''​ to send the zone changes to our name server.
 +
 +
 +<file bash /​etc/​dehydated/​hooks/​nsupdate>​
 +#​!/​usr/​bin/​env bash
 +
 +#
 +# Deployment script for DNS challenge using nsupdate
 +#
 +# Arguments: hook ACTION DOMAIN CTOKEN VTOKEN
 +#
 +#   ​ACTION: ​    The action the hook SHALL perform
 +#       ​clean_challenge ​        Clean the validation token from the domain
 +#       ​deploy_challenge ​       Deploy a new validation token for a domain
 +#       ​deploy_cert ​            ​Deploy a certificate for a domain
 +#       ​invalid_challenge ​      ???
 +#       ​request_failure ​        The request for a certificate failed
 +#
 +#   ​DOMAIN: ​    The domain to validate
 +#
 +#   ​CTOKEN: ​    The challenge token (unused with DNS-01)
 +#
 +#   ​VTOKEN: ​    The validation token that needs to be inserted into DNS
 +
 +set -e
 +set -u
 +set -o pipefail
 +
 +KEYDIR="/​etc/​dehydrated/​nsupdate"​
 +
 +SOA="​${2:​-default}"​
 +SOALIST="​${SOA}"​
 +
 +while [[ "​${SOA}"​ == *"​."​* ]]; do
 +    SOA="​${SOA#​*.}"​
 +    SOALIST="​${SOALIST} ${SOA}"​
 +done
 +
 +for SOA in ${SOALIST}; do
 +    KEYFILE="​${KEYDIR}/​${SOA}.acme.conf"​
 +    if [ -r "​${KEYFILE}"​ ]; then
 +        if [ -L "​${KEYFILE}"​ ]; then
 +            KEYFILE="​$(readlink -m ${KEYFILE})"​
 +            SOA="​${KEYFILE##​*/​}"​
 +            SOA="​${SOA%.acme.conf}"​
 +        fi
 +        break
 +    fi
 +    unset KEYFILE
 +done
 +
 +KEYFILE="​${KEYFILE:​-/​dev/​null}"​
 +NSUPDATE="​nsupdate -k ${KEYFILE}"​
 +DNSSERVER="​ns1.example.com"​
 +ZONE="​.acme.example.com"​
 +TTL=600
 +
 +CHALLENGE=$(printf "​_acme-challenge.%s%s."​ "​${SOA}"​ "​${ZONE}"​)
 +
 +case "​$1"​ in
 +    deploy_challenge)
 +        printf "​server %s\nupdate add %s %d IN TXT \"​%s\"​\nsend\n"​ "​${DNSSERVER}"​ "​${CHALLENGE}"​ "​${TTL}"​ "​${4}"​ | $NSUPDATE
 +        ;;
 +    clean_challenge)
 +        printf "​server %s\nupdate delete %s %d IN TXT \"​%s\"​\nsend\n"​ "​${DNSSERVER}"​ "​${CHALLENGE}"​ "​${TTL}"​ "​${4}"​ | $NSUPDATE
 +        ;;
 +    deploy_cert)
 +        # optional:
 +        # /​path/​to/​deploy_cert.sh "​$@"​
 +        ;;
 +    unchanged_cert)
 +        # do nothing for now
 +        ;;
 +    startup_hook)
 +        # do nothing for now
 +        ;;
 +    exit_hook)
 +        # do nothing for now
 +        ;;
 +esac
 +
 +exit 0
 +</​file>​
 +
 +This script will take the incoming primary domain name requested for the new certificate and try to locate the TSIG key to use with it. These are expected to be present in the path defined by ''​KEYDIR'',​ (''/​etc/​dehydrated/​nsupdate''​ by default) and have the name ''​${domain}.acme.conf''​. Subdomains are automatically recursed.
 +
 +In the case of multiple domains using the same key, the actual file is named for the SOA-root as defined above and other domains sharing it are symlinks to this file. In the example:
 +
 +<file bash>
 +cp Kexample.client1._acme-challenge.key client1.example.acme.conf
 +ln -s client2.example.acme.conf client1.example.acme.conf
 +</​file>​
 +
 +Examples:
 +  * ''​client1.example''​ -> found as ''​client1.example.acme.conf''​
 +  * ''​subdomain.client1.example'',​ try ''​client1.example''​ -> found as ''​client1.example.acme.conf''​
 +  * ''​subdomain2.client2.example'',​ try ''​client2.example'',​ follow link -> found as ''​client1.example.acme.conf''​
 +
 +The ''​nsupdate''​ request is then generated from the final key file name, in all of the examples this will be ''​_acme-challenge.client1.example.acme.example.com''​. This **must** be the same as configured in the ''​CNAME''​ records! This is why we're always using SOA roots, this makes it easier to check.
 +
 +
 +===== Summary =====
 +
 +This is everything required to run dehydrated in a way that doesn'​t pollute zone files. This even works across machines, for example if some domains are physically located on different hosts. Only dehydrated, the hook and key files for the domains managed by that host's dehydrated need to be copied.
 +
 +If something doesn'​t work, check ''​grant''​ rules in ''​update-policy'',​ validate that keys are known to bind9, check the requests performed by changing the ''​NSUPDATE''​ commandline to ''​nsupdate -d ...''​.
 +
 +
 +==== Resources ====
 +
 +  * [[https://​github.com/​dehydrated-io/​dehydrated/​wiki/​example-dns-01-nsupdate-script]]
 +  * [[https://​datatracker.ietf.org/​doc/​draft-ietf-acme-subdomains/​]]
  
pr/le-dns-delegate.txt · Zuletzt geändert: 2023/05/03 22:25 von martok