From 63f6a3181fea59360b2bfe430f5c798f88b22527 Mon Sep 17 00:00:00 2001 From: Lexi Winter Date: Wed, 4 Jun 2025 08:51:26 +0100 Subject: add a TSIG-based dns validation handler while here, reorganise and improve documentation a bit. --- Makefile | 6 ++-- acme.conf.5 | 44 ++++++-------------------- acme.conf.sample | 31 +++++++++++++++++++ dns.sh | 78 ++++++++++++++++++++++++++++++++++++++++++++++ dnsutils.sh | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++ domains.conf.5 | 23 +++++++------- domains.conf.sample | 45 ++------------------------- kerberos.sh | 89 ++++++++--------------------------------------------- lfacme-dns.5 | 60 ++++++++++++++++++++++++++++++++++++ lfacme-http.5 | 13 ++++++++ lfacme-kerberos.5 | 30 +++++++++++++----- 11 files changed, 330 insertions(+), 173 deletions(-) create mode 100644 dns.sh create mode 100644 dnsutils.sh create mode 100644 lfacme-dns.5 diff --git a/Makefile b/Makefile index e8fb006..6912dd1 100644 --- a/Makefile +++ b/Makefile @@ -10,10 +10,11 @@ MAN8DIR?= ${MANDIR}/man8 HOOKDIR?= ${CONFDIR}/hooks LIBMODE?= 0644 -LIB= init.sh +LIB= init.sh dnsutils.sh CHALLENGEMODE?= 0755 -CHALLENGE= http.sh \ +CHALLENGE= dns.sh \ + http.sh \ kerberos.sh BINMODE?= 0755 @@ -30,6 +31,7 @@ HOOK= example-hook.sh MANMODE?= 0644 MAN5= acme.conf.5 \ domains.conf.5 \ + lfacme-dns.5 \ lfacme-http.5 \ lfacme-kerberos.5 MAN8= lfacme-renew.8 \ diff --git a/acme.conf.5 b/acme.conf.5 index 269b99b..0f17377 100644 --- a/acme.conf.5 +++ b/acme.conf.5 @@ -10,9 +10,14 @@ .Sh DESCRIPTION The .Nm -file is a shell script used to configure the global behaviour of +file is used to configure the global behaviour of .Nm lfacme . -The following variables may be set: +Each option should be configured as a +.Xr sh 1 +variable assignment, i.e. +.Dq Ar option Ns = Ns Ar value . +.Pp +The following configuration variables are supported: .Bl -tag -width indent .It Va ACME_URL (Required.) @@ -28,39 +33,10 @@ The path to a directory containing hooks to invoke when issuing certificates .Xr domains.conf 5 ) . The default value is .Pa /usr/local/etc/lfacme/hooks . -.It Va ACME_HTTP_CHALLENGE_DIR -The directory to store ACME challenges when responding to an -.Dq http-01 -challenge with the -.Dq http -challenge handler. -This directory must be served at -.Dq /.well-known/acme-challenge -on any domain which will be validated with the -.Dq http -handler. -There is no default value; you must set this if you use the -.Dq http -handler. -.It Va ACME_KERBEROS_PRINCIPAL -The Kerberos principal to use when responding to a -.Dq dns-01 -challenge with the -.Dq kerberos -challenge handler. -The default value is -.Dq host/$(hostname) . -.It Va ACME_KERBEROS_KEYTAB -The Kerberos keytab to use when responding to a -.Dq dns-01 -challenge with the -.Dq kerberos -challenge handler. -The keytab must contain a Kerberos key for the principal configured in -.Va ACME_KERBEROS_PRINCIPAL . -The default value is -.Pa /etc/krb5.keytab . .El +.Pp +Additional configuration variables may be used by the ACME validation hooks; +refer to the manual page for each hook for more details. .Sh SEE ALSO .Xr domains.conf 5 , .Xr lfacme-renew 8 , diff --git a/acme.conf.sample b/acme.conf.sample index 86d8693..151e4c9 100644 --- a/acme.conf.sample +++ b/acme.conf.sample @@ -1,6 +1,11 @@ # This is a sample configuration file for lfacme. It is a shell script, # so you can include other files or call programs here if you like. +####################################################################### +# Base options. +# +# These options are used by lfacme itself. + ### ACME_URL # The URL of the ACME server. @@ -29,6 +34,12 @@ #ACME_HOOKDIR="/some/directory" +####################################################################### +# lfacme-http(5) options. +# +# These options are used for the "http" challenge. + + ### ACME_HTTP_CHALLENGE_DIR # When using the "http" challenge handler, this is the directory which contains # ACME challenges. This must be served at /.well-known/acme-challenge on any @@ -38,6 +49,25 @@ #ACME_HTTP_CHALLENGE_DIR="/var/www/acme-challenge" +####################################################################### +# lfacme-dns(5) options. +# +# These options are used for the "dns" challenge. + + +### ACME_DNS_KEYFILE +# Path to the TSIG key nsupdate will use to authenticate the update. +# No default; you must configure this when using the dns challenge. + +#ACME_DNS_KEYFILE="/path/to/key" + + +####################################################################### +# lfacme-kerberos(5) options. +# +# These options are used for the "kerberos" challenge. + + ### ACME_KERBEROS_PRINCIPAL # When using the "kerberos" challenge handler, this is the Kerberos principal # we use for nsupdate. The default is "host/$(hostname)", which assumes a @@ -45,6 +75,7 @@ #ACME_KERBEROS_PRINCIPAL="host/server.example.org@EXAMPLE.ORG" + ### ACME_KERBEROS_KEYTAB # When using the "kerberos" challenge handler, this is the keytab used to # issue the ticket. It must contain a key for $ACME_KERBEROS_PRINCIPAL. diff --git a/dns.sh b/dns.sh new file mode 100644 index 0000000..9b26bd3 --- /dev/null +++ b/dns.sh @@ -0,0 +1,78 @@ +#! /bin/sh +# This source code is released into the public domain. + +. /usr/local/share/lfacme/init.sh +. /usr/local/share/lfacme/dnsutils.sh + +# begin, done or failed +ACTION=$1 +# ACME method, must be dns-01. +METHOD=$2 +# This is the full domain name we're authorising. +DOMAIN=$3 +# Token name, not used for dns-01. +TOKEN=$4 +# The token value we need to create. +AUTH=$5 + +if [ "$#" -ne 5 ]; then + _fatal "missing arguments" +fi + +if [ "$METHOD" != "dns-01" ]; then + exit 1 +fi + +if [ -z "$ACME_DNS_KEYFILE" ]; then + _fatal "ACME_DNS_KEYFILE not configured" +fi + +# Add a new record using nsupdate. +_add_record() { + local domain="$1" + local auth="$2" + + nsupdate -k "$ACME_DNS_KEYFILE" < Run the hook '' after (re)issuing this certificate. -# If begins with a '/' then it is an absolute path, -# otherwise it is relative to $ACME_HOOKDIR. -# This option may be given multiple times. -# -# challenge= -# Use as the challenge handler. If begins -# with '/' then it is an absolute path, otherwise it will -# be searched for in /usr/local/share/lfacme/challenge/, -# then /usr/local/etc/lfacme/challenge/. -# -# The challenge handler is passed to uacme(1), so you can use -# any uacme-compatible handler here. -# -# Two handlers are supplied with lfacme: "http" and "kerberos". -# The default handler is "http". -## -# A certificate name of "*" can be used to set the default options for any -# following certificates. Each "*" line replaces the previous default -# options. You can specify subject alt names here as well. -# -# For example, to use RSA (instead of the default ECDSA) for all certificates: +# To use RSA (instead of the default ECDSA) for all certificates: * type=rsa + # To use HTTP for all challenges: * challenge=http diff --git a/kerberos.sh b/kerberos.sh index 2bbfd0f..08663d8 100644 --- a/kerberos.sh +++ b/kerberos.sh @@ -2,6 +2,7 @@ # This source code is released into the public domain. . /usr/local/share/lfacme/init.sh +. /usr/local/share/lfacme/dnsutils.sh # begin, done or failed ACTION=$1 @@ -30,36 +31,6 @@ if ! kinit -k -t "$ACME_KERBEROS_KEYTAB" "$ACME_KERBEROS_PRINCIPAL"; then _fatal "failed to obtain a Kerberos ticket" fi -# Keep removing labels from the name until we find one with nameservers. -_getnameservers() { - local domain="$1" - - local _trydomain="$domain" - while ! [ -z "$_trydomain" ]; do - if [ "$_trydomain" = "${_trydomain#*.}" ]; then - # If there are no dots in the domain, we couldn't - # find the nameservers. - break - fi - - # For CNAME records, a query for NS will return the CNAME. - # Therefore we have to check we actually got NS records. - local nameservers="$( - dig "$_trydomain" ns +noall +answer | \ - awk '$4 == "NS" { print $5 }' - )" - - if ! [ -z "$nameservers" ]; then - echo "$nameservers" - return - fi - - _trydomain="${_trydomain#*.}" - done - - _fatal "unable to find nameservers for %s" "$_trydomain" -} - # Add a new record using nsupdate. _add_record() { local domain="$1" @@ -84,56 +55,20 @@ EOF return $? } -# Wait for the DNS record to appear on a specific nameserver. -_wait_for_nameserver() { - local domain="$1" - local auth="$2" - local nameserver="$3" - - _verbose "waiting for nameserver %s" "$nameserver" - - local waited=0 - local waitlimit=60 - while sleep 1; do - waited=$((waited + 1)) - if [ "$waited" -ge "$waitlimit" ]; then - _error "timed out waiting for '%s' on '%s'" \ - "$domain" "$nameserver" - return 1 +case "$ACTION" in + begin) + if ! _add_record "$DOMAIN" "$AUTH"; then + _fatal "failed to add the DNS record for %s" "$DOMAIN" + exit 1 fi - local _rdatas="$( - dig "_acme-challenge.$domain" txt @$nameserver \ - +noall +answer \ - | awk '$4 == "TXT" { print $5 }' - )" - for rdata in $_rdatas; do - if [ "$rdata" = "\"$auth\"" ]; then - return 0 - fi - done - done -} - -# Wait for DNS servers to have the given record. -_wait_for_record() { - local domain="$1" - local auth="$2" - local nameservers="$(_getnameservers "$domain")" - - _verbose "waiting for the DNS record '%s' to be published" "$domain" - for ns in $nameservers; do - _wait_for_nameserver "$domain" "$auth" "$ns" || return 1 - done - - return 0 -} + if ! lfacme_dns_wait_for_record "$DOMAIN" "$AUTH"; then + _fatal "timed out waiting for the DNS record for '%s' to be published" \ + "$DOMAIN" + exit 1 + fi -case "$ACTION" in - begin) - _add_record "$DOMAIN" "$AUTH" \ - && _wait_for_record "$DOMAIN" "$AUTH" - exit $? + exit 0 ;; done|failed) diff --git a/lfacme-dns.5 b/lfacme-dns.5 new file mode 100644 index 0000000..dedf250 --- /dev/null +++ b/lfacme-dns.5 @@ -0,0 +1,60 @@ +.\" This source code is released into the public domain. +.Dd June 4, 2025 +.Dt LFACME-DNS 5 +.Os +.Sh NAME +.Nm lfacme-dns +.Nd validate an ACME challenge via TSIG DNS updates +.Sh SYNOPSIS +In +.Xr domains.conf 5 : +.Bd -ragged -offset indent +.Ar domain +challenge=dns +.Ed +.Sh DESCRIPTION +The +.Nm +challenge hook will respond to an ACME domain validation using a DNS-based +.Dq dns-01 +authorization with TSIG-authenticated Dynamic DNS updates. +To use this challenge hook, configure one or more domains with +.Dq challenge=dns +in +.Xr domains.conf 5 . +.Pp +The +.Dq dns-01 +challenge expects the authorization token to be created as a TXT record at the +DNS name +.Dq _acme-challenge. Ns Ar domain . +When +.Nm +responds to the challenge, it will use +.Xr nsupdate 1 +to create this record. +The DNS update will be sent to the zone's master server (determined by the +MNAME field in the SOA record), and will be authenticated using the TSIG +key file configured by +.Ar ACME_DNS_KEYFILE +in +.Xr acme.conf 5 . +.Pp +Once validation is complete, the previously created DNS record will be removed. +.Sh CONFIGURATION +The +.Nm +challenge hook supports the following configuration options in +.Xr acme.conf 5 : +.Bl -tag -width indent +.It Va ACME_DNS_KEYFILE +(Required.) +The key file that will be passed to +.Xr nsupdate 1 +to authenticate the DNS update. +.El +.Sh SEE ALSO +.Xr acme.conf 5 , +.Xr domains.conf 5 , +.Xr lfacme-renew 8 , +.Xr nsupdate 1 diff --git a/lfacme-http.5 b/lfacme-http.5 index c66f9f8..ed5ca8e 100644 --- a/lfacme-http.5 +++ b/lfacme-http.5 @@ -37,6 +37,19 @@ in .Xr acme.conf 5 . This directory must be mapped to the appropriate path on the web server for the challenge to succeed. +.Sh CONFIGURATION +The +.Nm +challenge hook supports the following configuration options in +.Xr acme.conf 5 : +.Bl -tag -width indent +.It Va ACME_HTTP_CHALLENGE_DIR +(Required.) +The directory to place the challenge tokens in. +The contents of this directory should be served at the path +.Dq /.well-known/acme-challenge +on the web server for the domain to be validated. +.El .Sh SEE ALSO .Xr acme.conf 5 , .Xr domains.conf 5 , diff --git a/lfacme-kerberos.5 b/lfacme-kerberos.5 index 06b5b00..27973c7 100644 --- a/lfacme-kerberos.5 +++ b/lfacme-kerberos.5 @@ -34,9 +34,9 @@ responds to the challenge, it will use .Xr nsupdate 1 with the .Fl g -flag (enable GSS-TSIG) to create this token. -The DNS update will be sent to the zone's master server (determined by the -MNAME field in the SOA record). +flag to create this token. +The DNS update will be sent to the zone's master server, as determined by the +MNAME field in the SOA record. .Pp Before sending the update, .Nm @@ -46,10 +46,26 @@ for the principal configured by .Ar ACME_KERBEROS_PRINCIPAL in .Xr acme.conf 5 . -The principal's key must exist in the Kerberos keytab configured by -.Ar ACME_KERBEROS_KEYTAB -(by default, -.Pa /etc/krb5.keytab ) . +.Sh CONFIGURATION +The +.Nm +challenge hook supports the following configuration options in +.Xr acme.conf 5 : +.Bl -tag -width indent +.It Va ACME_KERBEROS_PRINCIPAL +The Kerberos principal to authenticate as when sending the DNS update. +The default value is +.Dq host/$(hostname) , +which assumes a default realm has been configured in +.Pa /etc/krb5.conf . +Explicitly configuring the principal is recommended, but not required. +.It Va ACME_KERBEROS_KEYTAB +The keytab used to issue the Kerberos ticket. +This must contain a key for the principal configured by +.Va ACME_KERBEROS_PRINCIPAL . +The default value is +.Pa /etc/krb5.keytab . +.El .Sh SEE ALSO .Xr acme.conf 5 , .Xr domains.conf 5 , -- cgit v1.2.3