aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile6
-rw-r--r--acme.conf.544
-rw-r--r--acme.conf.sample31
-rw-r--r--dns.sh78
-rw-r--r--dnsutils.sh84
-rw-r--r--domains.conf.523
-rw-r--r--domains.conf.sample45
-rw-r--r--kerberos.sh89
-rw-r--r--lfacme-dns.560
-rw-r--r--lfacme-http.513
-rw-r--r--lfacme-kerberos.530
11 files changed, 330 insertions, 173 deletions
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" <<EOF
+update add _acme-challenge.${DOMAIN}. 300 IN TXT "${AUTH}"
+send
+EOF
+ return $?
+}
+
+# Remove an existing record using nsupdate.
+_remove_record() {
+ local domain="$1"
+ local auth="$2"
+
+ nsupdate -k "$ACME_DNS_KEYFILE" <<EOF
+update delete _acme-challenge.${DOMAIN}. 300 IN TXT "${AUTH}"
+send
+EOF
+ return $?
+}
+
+case "$ACTION" in
+ begin)
+ if ! _add_record "$DOMAIN" "$AUTH"; then
+ _fatal "failed to add the DNS record for %s" "$DOMAIN"
+ exit 1
+ fi
+
+ 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
+
+ exit 0
+ ;;
+
+ done|failed)
+ _remove_record "$DOMAIN" "$AUTH"
+ exit $?
+ ;;
+
+ *)
+ _fatal "unknown action: %s" "$ACTION"
+ ;;
+esac
diff --git a/dnsutils.sh b/dnsutils.sh
new file mode 100644
index 0000000..a1523ff
--- /dev/null
+++ b/dnsutils.sh
@@ -0,0 +1,84 @@
+# This source code is released into the public domain.
+#
+# Utility functions for DNS-based authorizations.
+
+# Retrieve the nameservers for a given domain. On failure, prints an error
+# message and exits.
+lfacme_dns_getnameservers() {
+ local domain="$1"
+
+ # Keep removing labels from the name until we find one with nameservers.
+ 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"
+}
+
+# Wait for the DNS record to appear on a specific nameserver.
+lfacme_dns_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
+ 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.
+lfacme_dns_wait_for_record() {
+ local domain="$1"
+ local auth="$2"
+ local nameservers="$(lfacme_dns_getnameservers "$domain")"
+
+ _verbose "waiting for the DNS record '%s' to be published" "$domain"
+ for ns in $nameservers; do
+ if ! lfacme_dns_wait_for_nameserver "$domain" "$auth" "$ns"; then
+ return 1
+ fi
+ done
+
+ return 0
+}
+
+
diff --git a/domains.conf.5 b/domains.conf.5
index ba65610..fd071e4 100644
--- a/domains.conf.5
+++ b/domains.conf.5
@@ -14,9 +14,9 @@ file is used to configure the certificates that
.Nm lfacme
will issue or renew.
Each line specifies one certificate as a series of whitespace-separated fields.
-The first field is the certificate name, which is used internally by
+The first field is the certificate name, which is used by
.Nm lfacme
-in the certificate filename but is not part of the certificate itself.
+to create the certificate filename but is not part of the certificate itself.
The remaining fields are either certificate options or subject alt names for
the certificate.
.Pp
@@ -63,24 +63,25 @@ The challenge script is passed to
.Xr uacme 1 ;
see the uacme documentation for details on the calling convention.
.Pp
-Two challenge scripts are provided with
+The following challenge scripts are provided with
.Nm lfacme :
.Bl -tag -width kerberos
.It Sy http
Use HTTP-based validation.
-This requires
-.Va ACME_HTTP_CHALLENGE_DIR
-to be set in
-.Xr acme.conf 5 .
+See
+.Xr lfacme-http 5 .
This is the default challenge handler.
+.It Sy dns
+Use DNS-based validation with
+.Xr nsupdate 1 .
+See
+.Xr lfacme-dns 5 .
.It Sy kerberos
Use DNS-based validation with
.Xr nsupdate 1
using Kerberos authentication.
-This requires
-.Va ACME_KERBEROS_PRINCIPAL
-to be set in
-.Xr acme.conf 5 .
+See
+.Xr lfacme-kerberos 5 .
.El
.It Sy hook Ns Li = Ns Ar filename
Invoke
diff --git a/domains.conf.sample b/domains.conf.sample
index 5eb66d4..59ce539 100644
--- a/domains.conf.sample
+++ b/domains.conf.sample
@@ -5,52 +5,13 @@
# syntax here.
#
# Empty lines and lines beginning with a '#' character are ignored.
-
-##
-# Each line specifies one certificate as a series of whitespace-separated
-# fields. The first field is the certificate name, which is used internally
-# by lfacme in the certificate filename, but is not part of the certificate
-# itself.
#
-# The remaining fields are either certificate options or subject alt names for
-# the certificate.
-#
-# If no subject alt names are provided, then the certificate name is used as
-# the common name and subject alt name. Otherwise, the first subject alt name
-# is used as the common name.
+# A few examples are provided here; see acme.conf(5) for complete documentation.
-##
-# Supported options:
-#
-# type=ec Generate a secp384r1 ECDSA private key.
-# (This is the default.)
-#
-# type=rsa Generate a 3072-bit RSA private key.
-#
-# hook=<name> Run the hook '<name>' after (re)issuing this certificate.
-# If <name> begins with a '/' then it is an absolute path,
-# otherwise it is relative to $ACME_HOOKDIR.
-# This option may be given multiple times.
-#
-# challenge=<name>
-# Use <name> as the challenge handler. If <name> 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 ,