Automate DNS Authentication with the Modern Certificate Renewal CLI Tool lego
Hello, I'm Munou.
Recently, I've been trying to move away from file-based authentication. To be honest, it feels a bit "off" that I'm running certificate renewals with certbot using file authentication, which requires me to have Nginx listen on a dummy mail.example.com domain.
It's fine since it runs automatically, but certbot works by temporarily placing files in a path for file authentication to perform the update. However, a mail server doesn't really need http, and I'd rather not accept it at all.
Furthermore, this operation falls apart if you have multiple origin destinations specified in the A record.
This is because:
$dig soulminingrig.com +short
91.98.169.80
163.44.113.145
When accepting requests on two IPs like this, if you run certbot on one server, the file for authentication won't exist on the other. It probably succeeds because the HTTP or DNS client sends the request to the nearest server, but relying on such "magic" is a failure in terms of operations.
Difficulties in implementing DNS authentication due to the specifications of acme.sh and certbot
DNS authentication itself is possible with both.
However, in the case of certbot, there are no presets to automatically call the DNS update API. Therefore, we have to handle all record registrations ourselves, making automatic renewal practically unusable for DNS authentication.
In the case of acme.sh, it doesn't support the ConoHa DNS API by default. Trying to use it with custom options makes the configuration quite chaotic. It becomes very difficult if the DNS server changes.
lego
While looking for alternatives, I surprisingly found a tool written in Go that supports the ConoHa DNS API.
ConoHa v3 :: Let’s Encrypt client and ACME library written in Go.
GitHub - go-acme/lego: Let's Encrypt/ACME client and library written in Go · GitHub
Amazing!
Installation
I'm grateful it's available in pkg too...
# pkg search lego
lego-4.33.0 Let's Encrypt client and ACME library written in Go
pkg install lego-4.33.0
Execution Script
In my case, most domains are managed on the Nginx side with only a few exceptions, so I had Chappy (ChatGPT) create the update script for me.
Since I want to issue SAN certificates for www.example.com and example.com without issuing wildcard certificates, it looks like this:
#!/bin/sh
set -eu
SITES_DIR="/usr/local/etc/nginx/sites-enabled"
export CONOHAV3_TENANT_ID=""
export CONOHAV3_API_USER_ID=""
export CONOHAV3_API_PASSWORD=""
export CONOHAV3_PROPAGATION_TIMEOUT="600"
export CONOHAV3_POLLING_INTERVAL="300"
LEGO="/usr/local/bin/lego"
SSLDIR="/usr/local/etc/ssl/lego"
EMAIL="taro@example.com"
DNS_PROVIDER="conohav3"
# Add domains here that you want to update even if they are not managed by nginx
# List multiple domains separated by spaces
EXTRA_DOMAINS="
mail.example.com
"
tmp_all="$(mktemp)"
tmp_done="$(mktemp)"
trap 'rm -f "$tmp_all" "$tmp_done"' EXIT INT TERM
# Extract from nginx server_name
grep -RhoE 'server_name[[:space:]]+[^;]+' "$SITES_DIR" \
| sed -E 's/^server_name[[:space:]]+//' \
| tr ' ' '\n' \
| sed 's/;$//' \
| sed '/^$/d' \
| sed '/^\*\./d' \
| sed '/^_/d' \
>> "$tmp_all"
printf '%s\n' "$EXTRA_DOMAINS" \
| tr ' ' '\n' \
| sed '/^$/d' \
>> "$tmp_all"
sort -u -o "$tmp_all" "$tmp_all"
: > "$tmp_done"
issue_cert() {
echo "==> issuing certificate for: $*"
"$LEGO" \
--accept-tos \
--path "${SSLDIR}" \
--email "$EMAIL" \
--dns "$DNS_PROVIDER" \
--dns.resolvers 1.1.1.1 \
"$@" \
run
}
already_done() {
grep -Fxq "$1" "$tmp_done"
}
mark_done() {
printf '%s\n' "$1" >> "$tmp_done"
}
while IFS= read -r host; do
[ -n "$host" ] || continue
if already_done "$host"; then
continue
fi
case "$host" in
www.*)
apex="${host#www.}"
if grep -Fxq "$apex" "$tmp_all"; then
issue_cert -d "$apex" -d "$host"
mark_done "$apex"
mark_done "$host"
else
issue_cert -d "$host"
mark_done "$host"
fi
;;
*.*.*)
issue_cert -d "$host"
mark_done "$host"
;;
*.*)
if grep -Fxq "www.$host" "$tmp_all"; then
issue_cert -d "$host" -d "www.$host"
mark_done "$host"
mark_done "www.$host"
else
issue_cert -d "$host"
mark_done "$host"
fi
;;
*)
echo "skip invalid host: $host" >&2
;;
esac
done < "$tmp_all"
To renew, just change the run option to renew.
Automatic Renewal
In my case, since I'm using FreeBSD, I'll add it to /etc/periodic.conf.
weekly_lego_enable="YES"
weekly_lego_renewscript="/usr/local/etc/lego/dns.sh"
weekly_lego_deployscript="/usr/local/etc/lego/deploy.sh"
Since there are default /usr/local/etc/lego/deploy.sh and /usr/local/etc/lego/lego.sh files, I think it's generally better to follow this design. However, as far as I can see, the default seems to use file-based authentication, so changes are necessary if you want to use DNS authentication like in this case.