diff options
| author | Lexi Winter <ivy@FreeBSD.org> | 2025-06-03 07:21:08 +0100 |
|---|---|---|
| committer | Lexi Winter <ivy@FreeBSD.org> | 2025-06-03 07:21:08 +0100 |
| commit | e1f2a8dd272cdbe4f5e54853008a3e5f24f4af71 (patch) | |
| tree | 2afe7daa22b636948498595a4b1e07b3fe9b2eea | |
| download | lfacme-e1f2a8dd272cdbe4f5e54853008a3e5f24f4af71.tar.gz lfacme-e1f2a8dd272cdbe4f5e54853008a3e5f24f4af71.tar.bz2 | |
initial commit
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | Makefile | 75 | ||||
| -rw-r--r-- | README | 55 | ||||
| -rw-r--r-- | acme.conf.5 | 39 | ||||
| -rw-r--r-- | acme.conf.sample | 33 | ||||
| -rw-r--r-- | domains.conf.5 | 105 | ||||
| -rw-r--r-- | domains.conf.sample | 50 | ||||
| -rw-r--r-- | example-hook.sh | 25 | ||||
| -rw-r--r-- | init.sh | 56 | ||||
| -rw-r--r-- | kerberos-challenge.sh | 146 | ||||
| -rw-r--r-- | lfacme-renew.8 | 26 | ||||
| -rw-r--r-- | lfacme-renew.sh | 229 | ||||
| -rw-r--r-- | lfacme-setup.8 | 20 | ||||
| -rw-r--r-- | lfacme-setup.sh | 8 |
14 files changed, 868 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..45d62d8 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.sw? diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6d83189 --- /dev/null +++ b/Makefile @@ -0,0 +1,75 @@ +PREFIX?= /usr/local +LIBDIR?= ${DESTDIR}/${PREFIX}/share/lfacme +BINDIR?= ${DESTDIR}/${PREFIX}/sbin +CONFDIR?= ${DESTDIR}/${PREFIX}/etc +MANDIR?= ${DESTDIR}/${PREFIX}/share/man +MAN5DIR?= ${MANDIR}/man5 +MAN8DIR?= ${MANDIR}/man8 +HOOKDIR?= ${CONFDIR}/hooks + +LIBMODE?= 0644 +LIB_FILES= init.sh \ + kerberos-challenge.sh + +BINMODE?= 0755 +BIN_FILES= lfacme-renew.sh \ + lfacme-setup.sh + +CONFMODE?= 0644 +CONF_FILES= acme.conf.sample \ + domains.conf.sample + +HOOKMODE?= 0755 +HOOK_FILES= example-hook.sh + +MANMODE?= 0644 +MAN5FILES= acme.conf.5 \ + domains.conf.5 +MAN8FILES= lfacme-renew.8 \ + lfacme-setup.8 + +default: all + +all: + @echo "Nothing to do." + +install: + @echo 'create ${LIBDIR}'; install -d ${LIBDIR}; \ + for lib in ${LIB_FILES}; do \ + echo "install ${LIBDIR}/$$lib"; \ + install -C -m ${LIBMODE} "$$lib" "${LIBDIR}/$$lib"; \ + done; \ + \ + echo 'create ${BINDIR}'; install -d ${BINDIR}; \ + for bin in ${BIN_FILES}; do \ + basename=$${bin%*.sh}; \ + echo "install ${BINDIR}/$$basename"; \ + install -C -m ${BINMODE} "$$bin" "${BINDIR}/$$basename"; \ + done; \ + \ + echo 'create ${CONFDIR}'; install -d ${CONFDIR}; \ + for conf in ${CONF_FILES}; do \ + echo "install ${CONFDIR}/$$conf"; \ + install -C -m ${CONFMODE} "$$conf" "${CONFDIR}/$$conf"; \ + done; \ + \ + echo 'create ${HOOKDIR}'; install -d ${HOOKDIR}; \ + for hook in ${HOOK_FILES}; do \ + basename=$${hook%*.sh}; \ + echo "install ${HOOKDIR}/$$basename"; \ + install -C -m ${HOOKMODE} "$$hook" "${HOOKDIR}/$$basename"; \ + done; \ + \ + echo 'create ${MANDIR}'; install -d ${MANDIR}; \ + \ + echo 'create ${MAN5DIR}'; install -d ${MAN5DIR}; \ + for man in ${MAN5FILES}; do \ + echo "install ${MAN5DIR}/$$man"; \ + install -C -m ${MANMODE} "$$man" "${MAN5DIR}/$$man"; \ + done; \ + \ + echo 'create ${MAN8DIR}'; install -d ${MAN8DIR}; \ + for man in ${MAN8FILES}; do \ + echo "install ${MAN8DIR}/$$man"; \ + install -C -m ${MANMODE} "$$man" "${MAN8DIR}/$$man"; \ + done; \ @@ -0,0 +1,55 @@ +lfacme: a simple ACME client based on uacme +------------------------------------------- + +lfacme is a wrapper around uacme to make it a bit more flexible. i wrote it +primarily for my own use, but you're welcome to use it too. + +currently, there is one major limitation: the only supported domain validation +method is dns-01 with Kerberized nsupdate. patches to improve this would be +welcome. + +it's only tested on FreeBSD and may or may not work on other platforms. +if it doesn't work, it shouldn't be difficult to port. + +requirements: + ++ POSIX-compatible /bin/sh ++ uacme (in FreeBSD: security/uacme) ++ OpenSSL command-line tool ++ BIND's "dig" and "nsupdate" (in FreeBSD: dns/bind-tools) ++ Kerberos kinit (either MIT or Heimdal should work) + +install: + +# make install [DESTDIR=/some/where] + +usage: + ++ make sure /etc/krb5.keytab exists since this will be used to issue the + Kerberos ticket for domain validation. ++ create the config files (see below): + /usr/local/etc/uacme/acme.conf and + /usr/local/etc/uacme/domains.conf ++ run "lfacme-setup" to create an ACME account ++ run "lfacme-renew" to issue certificates ++ put "lfacme-renew" in cron if you want to renew certificates automatically. + it's fine to run this once a day, since it won't renew certificates unless + they're going to expire soon. + +known issues: + ++ lfacme assumes it's installed in /usr/local. if you want to change this, + you'll need to edit the scripts. + ++ we disable ARI in uacme (uacme --no-ari) because it's broken on non-glibc + platforms. this is a uacme bug: https://github.com/ndilieto/uacme/issues/91 + +config files: + +there are two configuration files: + ++ acme.conf configures the global behaviour of lfacme ++ domains.conf lists the certificates lfacme should issue + +these both come with manual pages which explain how to configure them, +and sample configs are provided. diff --git a/acme.conf.5 b/acme.conf.5 new file mode 100644 index 0000000..9b632b5 --- /dev/null +++ b/acme.conf.5 @@ -0,0 +1,39 @@ +.\" This source code is released into the public domain. +.Dd June 3, 2025 +.Dt ACME.CONF 5 +.Os +.Sh NAME +.Nm acme.conf +.Nd lfacme global configuration file +.Sh SYNOPSIS +.Pa /usr/local/etc/lfacme/acme.conf +.Sh DESCRIPTION +The +.Nm +file is a shell script used to configure the global behaviour of +.Nm lfacme . +The following variables may be set: +.Bl -tag -width indent +.It Va ACME_URL +(Required.) +The URL of the ACME server. +.It Va ACME_DIR +(Required.) +The path to the base configuration directory, where certificates will be stored. +.It Va ACME_HOOKDIR +The path to a directory containing hooks to invoke when issuing certificates +(see +.Xr domains.conf 5 ) . +The default value is +.Pa ${ACME_DIR}/hooks . +.It Va ACME_KERBEROS_PRINCIPAL +The Kerberos principal to use when responding to a +.Dq dns-01 +challenge. +The default value is +.Dq host/$(hostname) . +.El +.Sh SEE ALSO +.Xr domains.conf 5 , +.Xr lfacme-setup 8 , +.Xr lfacme-renew 8 diff --git a/acme.conf.sample b/acme.conf.sample new file mode 100644 index 0000000..f6f8432 --- /dev/null +++ b/acme.conf.sample @@ -0,0 +1,33 @@ +# 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. + + +### ACME_URL: The URL of the ACME server. +# No default, you must set this. + +# Let's Encrypt production:: +#ACME_URL="https://acme-v02.api.letsencrypt.org/directory" + +# Let's Encrypt staging: +#ACME_URL="https://acme-staging-v02.api.letsencrypt.org/directory" + + +### ACME_DIR: The path to lfacme's configuration directory. +# This is also where generated certificates are stored. +# No default, you must set this. + +#ACME_DIR="/usr/local/etc/acme" + + +### ACME_HOOKDIR: The path to the directory containing certificate hooks. +# The default is "${ACME_DIR}/hooks". +# There is usually no need to change this. + +#ACME_HOOKDIR="/some/directory" + + +### ACME_KERBEROS_PRINCIPAL: The Kerberos principal we use for nsupdate. +# The default is "host/$(hostname)", which assumes a default realm is +# configured in /etc/krb5.conf. + +#ACME_KERBEROS_PRINCIPAL="host/server.example.org@EXAMPLE.ORG" diff --git a/domains.conf.5 b/domains.conf.5 new file mode 100644 index 0000000..c4e1966 --- /dev/null +++ b/domains.conf.5 @@ -0,0 +1,105 @@ +.\" This source code is released into the public domain. +.Dd June 3, 2025 +.Dt DOMAINS.CONF 5 +.Os +.Sh NAME +.Nm domains.conf +.Nd lfacme domains configuration file +.Sh SYNOPSIS +.Pa /usr/local/etc/lfacme/domains.conf +.Sh DESCRIPTION +The +.Nm +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 +.Nm lfacme +in the certificate filename, but is not part of the certificate itself. +The remaining fields are certificate options, which may be either subject alt +names or options for the certificate. +.Pp +If no subject alt names are provided, then the certificate name is used as +the common name and subject alt name. +.Pp +The following options may be set: +.Bl -tag -width indent +.It Sy type Ns Li = Ns Ar keytype +Configure the private key type. +The +.Ar keytype +argument may be +.Dq ec +to generate a secp384r1 ECDSA key, or +.Dq rsa +to generate a 3072-bit RSA key. +If not specified, the default value is +.Dq ec . +.It Sy hook Ns Li = Ns Ar filename +Invoke +.Ar filename +when this certificate is issued or renewed. +If +.Ar filename +begins with a +.Sq / +character, then it is assumed to be an absolute path, +otherwise it is relative to the +.Va ACME_HOOKDIR +configured in +.Xr acme.conf 5 . +This option may be specified multiple times. +.Pp +The hook will be called with a single argument, +which may be one of the following: +.Bl -tag -width newcert +.It Sy newcert +A certificate has been issued or renewed. +.El +.Pp +The following environment variables will be when running the hook script: +.Bl -tag -width LFACME_CERTFILE +.It Sy LFACME_CERT +The identifier of the certificate, i.e. the first field in +.Nm . +This is not necessarily the certificate's common name. +.It Sy LFACME_CERTFILE +The path of a file which contains the public certificate and any issuer +certificates, in PEM format. +.It Sy lFACME_KEYFILE +The path of a file which contains the private key file in PEM format. +.El +.El +.Sh EXAMPLES +Issue a certificate for +.Dq example.org +using the default options. +We don't provide any SANs, so the certificate name is used as the domain. +.Bd -literal -offset indent +example.org +.Ed +.Pp +Issue a certificate for +.Dq example.org +with some SANs. +Notice that because we specify one SAN, we now have to specify all of them. +.Bd -literal -offset indent +example.org example.org www.example.org +.Ed +.Pp +Issue two certificates for an SMTP server, one EC and one RSA. +Some older SMTP clients still don't like EC certs. +Run a hook after the certificate is (re)issued. +.Bd -literal -offset indent +smtp-ec smtp.example.org type=ec hook=install-smtp-cert +smtp-rsa smtp.example.org type=rsa hook=install-smtp-cert +.Ed +.Pp +Issue a certificate for a server and run multiple hooks. +.Bd -literal -offset indent +server.example.org hook=nginx hook=postfix hook=node-exporter +.Ed +.Sh SEE ALSO +.Xr acme.conf 5 , +.Xr lfacme-renew 8 diff --git a/domains.conf.sample b/domains.conf.sample new file mode 100644 index 0000000..dbcca4a --- /dev/null +++ b/domains.conf.sample @@ -0,0 +1,50 @@ +## +# Domains configuration file for lfacme. +# +# This is NOT a shell script (unlike acme.conf) so you cannot use shell +# syntax here. +# +# Empty lines and lines beginning with a '#' character are ignored. + +## +# Each line specifies one certificate using one or more whitespace-separated +# fields. +# +# The first field is the certificate name, which is only used internally by +# lfacme and is not part of the certificate. +# +# The remaining fields are certificate options, which may be either subject alt +# names or options for the certificate. +# +# If no subject alt names are provided, then the certificate name is used as +# the common name and subject alt name. + +## +# 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. + +# Issue a cert for example.org using the default options. +# We don't provide any SANs, so the certificate name is used as the domain. +example.org + +# Issue a cert for example.org with some SANs. +# Notice that because we specify one SAN, we now have to specify all of them. +example.org example.org www.example.org + +# Issue two certs for an SMTP server, one EC and one RSA. +# Some older SMTP clients still don't like EC certs. +# Run a hook after the certificate is (re)issued. +smtp-ec smtp.example.org type=ec hook=install-smtp-cert +smtp-rsa smtp.example.org type=rsa hook=install-smtp-cert + +# Issue a certificate for a server and run multiple hooks. +server.example.org hook=nginx hook=postfix hook=node-exporter diff --git a/example-hook.sh b/example-hook.sh new file mode 100644 index 0000000..01c5644 --- /dev/null +++ b/example-hook.sh @@ -0,0 +1,25 @@ +#! /bin/sh +# An example hook. + +# Action is always 'newcert', at least for now. +action="$1" + +# Environment variables: +# $LFACME_CERT is the name of the certificate +# $LFACME_CERTFILE is the filename of the certificate. +# $LFACME_KEYFILE is the filename of the private key. + +set -e + +case "$action" in +newcert) + # The certificate was issued or renewed. + cp "$LFACME_CERTFILE" /usr/local/etc/nginx/tls/cert.pem + cp "$LFACME_KEYFILE" /usr/local/etc/nginx/tls/key.pem + nginx -s reload + ;; + +*) + # Ignore unknown actions, because new ones might be added later. + ;; +esac @@ -0,0 +1,56 @@ +# This source code is released into the public domain. + +_BASEDIR="/usr/local" +_SHARE="${_BASEDIR}/share/lfacme" +_CONFDIR="${_BASEDIR}/etc/lfacme" +_CONFIG="${_CONFDIR}/acme.conf" +_DOMAINS="${_CONFDIR}/domains.conf" +_UACME=/usr/local/bin/uacme +_UACME_DIR="${_CONFDIR}/certs" + +_PROGNAME="$0" + +_uacme() { + "$_UACME" -a "$ACME_URL" -c "$_UACME_DIR" "$@" +} + +_fatal() { + local _fmt=$1; shift + local _msg="$(printf "$_fmt" "$@")" + printf >&2 '%s: FATAL: %s\n' "$_PROGNAME" "$_msg" + exit 1 +} + +_error() { + local _fmt=$1; shift + local _msg="$(printf "$_fmt" "$@")" + printf >&2 '%s: ERROR: %s\n' "$_PROGNAME" "$_msg" +} + +_warn() { + local _fmt=$1; shift + local _msg="$(printf "$_fmt" "$@")" + printf >&2 '%s: WARNING: %s\n' "$_PROGNAME" "$_msg" +} + +if ! [ -f "$_CONFIG" ]; then + _fatal "missing %s" "$_CONFIG" +fi + +. "$_CONFIG" + +if [ -z "$ACME_URL" ]; then + _fatal "ACME_URL must be set in %s" "$_CONFIG" +fi + +if [ -z "$ACME_DIR" ]; then + _fatal "ACME_DIR must be set in %s" "$_CONFIG" +fi + +if [ -z "$ACME_KERBEROS_PRINCIPAL" ]; then + ACME_KERBEROS_PRINCIPAL="host/$(hostname)" +fi + +if [ -z "$ACME_HOOKDIR" ]; then + ACME_HOOKDIR="${_CONFDIR}/hooks" +fi diff --git a/kerberos-challenge.sh b/kerberos-challenge.sh new file mode 100644 index 0000000..95ca2af --- /dev/null +++ b/kerberos-challenge.sh @@ -0,0 +1,146 @@ +#! /bin/sh +# This source code is released into the public domain. + +. /usr/local/share/lfacme/init.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 + +#set +#exit 1 + +if [ "$#" -ne 5 ]; then + _fatal "missing arguments" +fi + +if [ "$METHOD" != "dns-01" ]; then + _warn "skip method %s" "$METHOD" + exit 1 +fi + +if ! kinit -k -t /etc/krb5.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" + local auth="$2" + + nsupdate -g <<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 -g <<EOF +update delete _acme-challenge.${DOMAIN}. 300 IN TXT "${AUTH}" +send +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" + + echo "waiting for $domain on nameserver $ns..." + + waited=0 + waitlimit=60 + while sleep 1; do + waited=$((waited + 1)) + if [ $waited -ge $waitlimit ]; then + _error "timed out waiting for nameserver update for %s" \ + "$domain" + return 1 + fi + + data="$(dig "_acme-challenge.$domain" txt @$nameserver +short)" + if [ -z "$data" ]; then + continue + fi + + if [ "$data" = "\"$auth\"" ]; then + return 0 + fi + done +} + +# Wait for DNS servers to have the given record. +_wait_for_record() { + local domain="$1" + local auth="$2" + local nameservers="$(_getnameservers "$domain")" + + for ns in $nameservers; do + _wait_for_nameserver "$domain" "$auth" "$ns" || return 1 + done + + return 0 +} + +case "$ACTION" in + begin) + _add_record "$DOMAIN" "$AUTH" \ + && _wait_for_record "$DOMAIN" "$AUTH" + exit $? + ;; + + done|failed) + _remove_record "$DOMAIN" "$AUTH" + exit $? + ;; + + *) + _fatal "unknown action: %s" "$ACTION" + ;; +esac + + diff --git a/lfacme-renew.8 b/lfacme-renew.8 new file mode 100644 index 0000000..e198dc2 --- /dev/null +++ b/lfacme-renew.8 @@ -0,0 +1,26 @@ +.\" This source code is released into the public domain. +.Dd June 3, 2025 +.Dt LFACME-RENEW 8 +.Os +.Sh NAME +.Nm lfacme-renew +.Nd issue or renew ACME certificates +.Sh SYNOPSIS +.Nm +.Sh DESCRIPTION +The +.Nm +utility examines the ACME certificates configured in +.Xr domains.conf 5 . +If a certificate was previously issued and is still valid for longer than 30 +days, it will be ignored. +Otherwise, the certificate will be issued or renewed and any configured hook +scripts will be invoked. +.Pp +An ACME account must be created using +.Xr lfacme-setup 8 +before running +.Nm . +.Sh SEE ALSO +.Xr domains.conf 5 , +.Xr lfacme-setup 8 diff --git a/lfacme-renew.sh b/lfacme-renew.sh new file mode 100644 index 0000000..e29788c --- /dev/null +++ b/lfacme-renew.sh @@ -0,0 +1,229 @@ +#! /bin/sh +# This source code is released into the public domain. + +. /usr/local/share/lfacme/init.sh + +if ! [ -d "$_UACME_DIR" ]; then + _fatal "run lfacme-setup first" +fi + +if ! [ -f "$_DOMAINS" ]; then + _fatal "missing $_DOMAINS" +fi + +args=$(getopt v $*) +if [ $? -ne 0 ]; then + exit 1 +fi +set -- $args + +# ARI is broken due to https://github.com/ndilieto/uacme/issues/91 +_uacme_flags="--no-ari" + +while :; do + case "$1" in + -v) + _uacme_flags="$_uacme_flags -v" + shift;; + --) + shift; break;; + esac +done + +# Create a key if it doesn't already exist. It would be better to always +# create a new key here, but currently uacme doesn't have a way to tell us +# that we need to do that. +_make_key() { + local keytype="$1" + local keyfile="$2" + + if [ -s "$keyfile" ]; then + return 0 + fi + + local _umask=$(umask) + umask 077 + + case $keytype in + ec) openssl ecparam -name secp384r1 -genkey -noout -out "$keyfile";; + rsa) openssl genrsa -out "$keyfile" 3072;; + *) _error "%s: unknown key type %s?" "$keyfile" "$keytype" + return 1;; + esac + + local _ret=$? + umask $_umask + + return $_ret +} + +# Create a new CSR for a domain. +_make_csr() { + local csrfile="$1" + local keyfile="$2" + local domain="$3" + local altnames="$4" + local csrconf="${csrfile}.cnf" + + cat >"$csrconf" <<EOF +[req] +distinguished_name = req_distinguished_name +req_extensions = req_ext +prompt = no + +[req_distinguished_name] +commonName = $domain + +[req_ext] +subjectAltName = @alt_names + +[alt_names] +DNS.1 = $domain +EOF + + local _i=2 + for altname in $altnames; do + printf >>"$csrconf" 'DNS.%d = %s\n' "$_i" "$altname" + _i=$((_i + 1)) + done + + # Generate the CSR + openssl req -new -key "$keyfile" -out "$csrfile" -config "$csrconf" + return $? +} + +# Process a single cert. +_docert() { + local identifier="$1"; shift + + # uacme creates the cert name by stripping the extension from the + # CSR filename, so the basename has to match the identifier. + local dir="${_UACME_DIR}/${identifier}" + local keyfile="${dir}/${identifier}-key.pem" + local csrfile="${dir}/${identifier}.csr" + local certfile="${dir}/${identifier}-cert.pem" + + # these can be overridden by args + local keytype="ec" + local altnames="" + local hooks="" + local domain="" + + # parse arguments for this cert + while ! [ -z "$1" ]; do + case "$1" in + type=rsa) keytype=rsa;; + type=ec) keytype=ec;; + type=*) _error "%s: unknown key type: %s" \ + "$identifier" "${1#type=*}" + return 1;; + hook=*) hooks="$hooks ${1#hook=*}";; + *=*) _error "%s: unknown option: %s" \ + "$identifier" "$1" + return 1;; + *.*) altnames="$altnames $1" + # Take the domain from the first altname. + if [ -z "$domain" ]; then + domain="$1" + fi + ;; + *) _error "%s: unknown option: %s" \ + "$identifier" "$1" + return 1;; + esac + shift + done + + # If no altnames were given, the identifier is the domain. + if [ -z "$domain" ]; then + domain="$identifier" + fi + + # make sure all the hook scripts are valid. if the hook name + # begins with a '/' it's a full path, otherwise it's related to + # ACME_HOOKDIR. + local _rhooks="" + for hook in $hooks; do + if [ "${hook#/*}" = "$hook" ]; then + hook="${ACME_HOOKDIR}/$hook" + fi + + if ! [ -f "$hook" ]; then + _error "%s: hook does not exist: %s" \ + "$identifier" "$hook" + return 1 + fi + + if ! [ -x "$hook" ]; then + _error "%s: hook is not executable: %s" \ + "$identifier" "$hook" + return 1 + fi + + _rhooks="$_rhooks $hook" + done + + mkdir -p -m0700 "$dir" + + if ! _make_key "$keytype" "$keyfile"; then + _error "%s: could not create a new private key" "$identifier" + return 1 + fi + + if ! _make_csr "$csrfile" "$keyfile" "$domain" "$altnames"; then + _error "%s: could not create the certificate signing request" \ + "$identifier" + return 1 + fi + + _uacme $_uacme_flags \ + -h "${_SHARE}/kerberos-challenge.sh" \ + issue "$csrfile" + _ret=$? + + # exit 1 means the cert wasn't reissued + if [ "$_ret" -eq 1 ]; then + return 0 + fi + + # exit 2 means an actual error + if [ "$_ret" -eq 2 ]; then + _error "%s: failed to issue certificate" "$identifier" + return 1 + fi + + # any other non-zero exit code is unexpected + if [ "$_ret" -ne 0 ]; then + _error "%s: unexpected exit code from uacme: %d" \ + "$identifier" "$_ret" + return 1 + fi + + # otherwise, exit code is 0 which means we (re)issued the cert, + # so run the hooks. + for hook in $_rhooks; do + env "LFACME_CERT=${identifier}" \ + "LFACME_KEYFILE=${keyfile}" \ + "LFACME_CERTFILE=${certfile}" \ + $hook newcert + if [ "$?" -ne 0 ]; then + _warn "%s: hook script '%s' failed" \ + "$identifier" "$hook" + fi + # should we do anything if the hook failed? + done + + return $? +} + +_exit=0 + +cat "$_DOMAINS" \ +| egrep -v '^(#|[[:space:]]*$)' \ +| while read identifier args; do + if ! _docert "$identifier" $args; then + _exit=1 + fi +done + +exit $_exit diff --git a/lfacme-setup.8 b/lfacme-setup.8 new file mode 100644 index 0000000..f6c51ca --- /dev/null +++ b/lfacme-setup.8 @@ -0,0 +1,20 @@ +.\" This source code is released into the public domain. +.Dd June 3, 2025 +.Dt LFACME-SETUP 8 +.Os +.Sh NAME +.Nm lfacme-setup +.Nd create a new ACME account +.Sh SYNOPSIS +.Nm +.Sh DESCRIPTION +The +.Nm +utility will register a new account with the ACME provider configured in +.Xr acme.conf 5 . +If the provider requires accepting terms of service to create an account, +the ToS URL will be printed and +.Nm +will prompt the user to accept them. +.Sh SEE ALSO +.Xr acme.conf 5 diff --git a/lfacme-setup.sh b/lfacme-setup.sh new file mode 100644 index 0000000..c2a0798 --- /dev/null +++ b/lfacme-setup.sh @@ -0,0 +1,8 @@ +#! /bin/sh +# This source code is released into the public domain. + +. /usr/local/share/lfacme/init.sh + +mkdir -p "$_UACME_DIR" + +_uacme new |
