TLS certificates: Jarl obtains and renews them over ACME
Jarl is now an ACME client (lego's core, no DNS providers). It answers DNS-01 challenges in a zone it serves, or through Porkbun's DNS API while Porkbun serves the domain; "auto" decides from the domain's public name servers at every renewal, and a CNAME on _acme-challenge is followed. Certificates are used for jarld's HTTPS (served without a restart; each port speaks HTTPS and plain HTTP, plain HTTP from loopback and trusted proxies still served) and/or written for named's DNS over TLS/HTTPS, with rndc reconfig on renewal. Renewal follows the CA's ARI window or two thirds of the lifetime, with back-off. Certificates you renew yourself can be used too. Certificates page and dashboard card, jarl cert, /api/v1/certificates, Prometheus gauges, docs/certificates.md, and an e2e section against Pebble. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
88965096bb
commit
88f63ffb0a
43 files changed
+4584
-45
No files matched your search
@@ -31,8 +31,22 @@ mkdir -p $E/legacy $E/cache $E/data
|
||||
JARLD=$E/jarld J=$E/jarl
|
||||
(cd "$ROOT" && go build -o $JARLD ./cmd/jarld && go build -o $J ./cmd/jarl) || exit 1
|
||||
NAMED_PID= JARLD_PID= LISTS_PID=
|
||||
# Certificates are tested against Pebble, Let's Encrypt's test CA, in
|
||||
# Docker (PEBBLE=0 skips them). It uses this test named to check
|
||||
# challenges, and the host's network.
|
||||
PEBBLE=${PEBBLE:-}
|
||||
if [ "$PEBBLE" != 0 ] && command -v docker >/dev/null && docker info >/dev/null 2>&1; then
|
||||
PEBBLE=jarl-e2e-pebble-$HTTP
|
||||
docker rm -f $PEBBLE >/dev/null 2>&1
|
||||
docker run -d --name $PEBBLE --network host -e PEBBLE_VA_NOSLEEP=1 ghcr.io/letsencrypt/pebble:latest \
|
||||
-config test/config/pebble-config.json -dnsserver 127.0.0.1:$DNS >/dev/null || PEBBLE=
|
||||
[ -n "$PEBBLE" ] && docker cp $PEBBLE:/test/certs/pebble.minica.pem $E/pebble-minica.pem >/dev/null
|
||||
else
|
||||
PEBBLE=
|
||||
fi
|
||||
cleanup() {
|
||||
[ -n "$LISTS_PID" ] && kill $LISTS_PID 2>/dev/null
|
||||
[ -n "$PEBBLE" ] && docker rm -f $PEBBLE >/dev/null 2>&1
|
||||
if [ "${KEEP:-}" = 1 ]; then echo "left running in $WORK (named $NAMED_PID, jarld $JARLD_PID)"; return; fi
|
||||
[ -n "$JARLD_PID" ] && kill $JARLD_PID 2>/dev/null
|
||||
[ -n "$NAMED_PID" ] && kill $NAMED_PID 2>/dev/null
|
||||
@@ -113,6 +127,14 @@ metrics:
|
||||
listen: ["127.0.0.1:$METRICS"]
|
||||
token: e2e-metrics-token-0123456789
|
||||
EOF
|
||||
if [ -n "$PEBBLE" ]; then cat >> $E/jarl.yaml <<EOF
|
||||
acme:
|
||||
account: {directory: "https://localhost:14000/dir", agree_tos: true}
|
||||
resolvers: ["127.0.0.1:$DNS"]
|
||||
ca_bundle: $E/pebble-minica.pem
|
||||
check_propagation: false # lego would ask the name servers on port 53
|
||||
EOF
|
||||
fi
|
||||
cd $E
|
||||
echo "secret-pass-123" | $JARLD -c jarl.yaml init --password-stdin --token --nameservers ns1.adbraun.test. --contact admin@adbraun.test > init.out 2>&1 || { cat init.out; exit 1; }
|
||||
grep -o 'jarl_[A-Za-z0-9_]*' init.out | head -1 > admin.token
|
||||
@@ -678,6 +700,43 @@ contains "^401$"
|
||||
run "metrics on the socket" 0 curl -sf --unix-socket $SOCK http://jarl/metrics
|
||||
contains "^jarl_open_changesets "
|
||||
|
||||
# TLS certificates (docs/certificates.md), from Pebble.
|
||||
if [ -n "$PEBBLE" ]; then
|
||||
DOT=${DOT_PORT:-25854}
|
||||
curl -sf --cacert $E/pebble-minica.pem https://localhost:15000/roots/0 > $E/pebble-root.pem
|
||||
run "cert check: the challenge goes to Jarl's zone" 0 $J cert check --domain ns1.adbraun.test --domain '*.adbraun.test'
|
||||
contains "ns1.adbraun.test *jarl *_acme-challenge.ns1.adbraun.test"
|
||||
run "cert check: a name nobody serves (exit 1)" 1 $J cert check --domain www.nowhere.invalid
|
||||
contains "www.nowhere.invalid *none"
|
||||
run "cert create: obtained from the CA" 0 $J cert create ns1 --domain ns1.adbraun.test --domain '*.adbraun.test' --use web --use dns
|
||||
contains "Status: *ok"
|
||||
contains "Challenges: .*ns1.adbraun.test: jarl (zone adbraun.test)"
|
||||
run "the key for named is group-readable only" 0 stat -c %a $E/etc-jarl/tls/ns1/privkey.pem
|
||||
contains "^640$"
|
||||
expect_dig "the challenge records are gone" "" _acme-challenge.ns1.adbraun.test TXT
|
||||
run "jarld serves it over HTTPS" 0 curl -sf --cacert $E/pebble-root.pem --resolve ns1.adbraun.test:$HTTP:127.0.0.1 https://ns1.adbraun.test:$HTTP/healthz
|
||||
run "plain HTTP from this host still works" 0 curl -sf http://127.0.0.1:$HTTP/healthz
|
||||
run "named: tls block with the certificate" 0 $J option set tls "e2e { cert-file \"$E/etc-jarl/tls/ns1/fullchain.pem\"; key-file \"$E/etc-jarl/tls/ns1/privkey.pem\"; }" --toplevel
|
||||
run "named: a DoT listener" 0 $J option set listen-on "port $DOT tls e2e { 127.0.0.1; }" --add
|
||||
run "DNS over TLS answers, with Jarl's certificate" 0 $DIG +tls +tls-ca=$E/pebble-root.pem +tls-hostname=ns1.adbraun.test +short -p $DOT @127.0.0.1 nas.adbraun.test A
|
||||
contains "^192.168.1.20$"
|
||||
serial() { echo | openssl s_client -connect 127.0.0.1:$1 -servername ns1.adbraun.test 2>/dev/null | openssl x509 -noout -serial; }
|
||||
before=$(serial $DOT)
|
||||
run "cert issue: renewed" 0 $J cert issue ns1
|
||||
after=$(serial $DOT)
|
||||
if [ -n "$before" ] && [ "$before" != "$after" ] && [ "$after" = "$(serial $HTTP)" ]; then ok "named and jarld serve the renewed certificate"; else bad "renewed certificate not served: named $before -> $after, jarld $(serial $HTTP)"; fi
|
||||
run "cert list" 0 $J cert list
|
||||
contains "ns1 *acme *dns,web *ns1.adbraun.test \\*.adbraun.test .* ok"
|
||||
run "a second certificate for the web UI is refused (exit 6)" 6 $J cert create other --domain nas.adbraun.test --use web
|
||||
run "certificate metrics" 0 curl -sf http://127.0.0.1:$HTTP/metrics
|
||||
contains '^jarl_certificate_failing{certificate="ns1",source="acme"} 0$'
|
||||
dot_id=$($J option list -o field=id,name,value | awk -F'\t' '$2 == "listen-on" && /tls e2e/ {print $1}')
|
||||
run "named: remove the DoT listener" 0 $J option unset --id "$dot_id"
|
||||
run "named: remove the tls block" 0 $J option unset tls --toplevel
|
||||
run "cert delete" 0 $J cert delete ns1 --yes
|
||||
run "its files are gone" 1 test -e $E/etc-jarl/tls/ns1
|
||||
fi
|
||||
|
||||
run "status at the end" 0 $J status
|
||||
contains "Drift:.*none"
|
||||
|
||||
|
||||
Reference in new issue
Block a user