caddy: add caddy-dns/rfc2136 + test-rfc2136 site -- self-hosted ACME flow

Wires Caddy as the ACME client side of our new self-hosted DNS-01
flow. Proves the design end-to-end: caddy-dns/rfc2136 -> our
CoreDNS rfc2136 plugin -> zone file write -> git auto-commit -> HE
AXFR -> LE validates -> cert issued.

Changes:
- caddy/Dockerfile: --with github.com/caddy-dns/rfc2136 added
  alongside the existing caddy-dns/vultr.
- caddy/Caddyfile: new test-rfc2136.supported.systems site that uses
  the new provider. server coredns:53 (docker internal), key from
  env, propagation_delay 60s + timeout 600s to accommodate HE pull.
- docker-compose.yml: ACME_TSIG_SECRET passed to the caddy container
  (the same secret CoreDNS verifies on the other side of the loop).

First cert issued in production: 2026-05-21 ~13:23 UTC. ~5.5 min
end-to-end from Caddy starting to cert in hand. Documented in
session notes; the cert sits unused in caddy-data/ until/unless
something publishes ports 80/443 for that hostname.
This commit is contained in:
Ryan Malloy 2026-05-21 13:27:05 -06:00
parent 18aa53bdc7
commit cc33fcbcc8
3 changed files with 35 additions and 1 deletions

View File

@ -38,3 +38,33 @@
# "Caddy is alive" sanity check inside the compose network. # "Caddy is alive" sanity check inside the compose network.
respond "CoreDNS DoT/DoH endpoint. DoT: port 853. DoH: /dns-query" 200 respond "CoreDNS DoT/DoH endpoint. DoT: port 853. DoH: /dns-query" 200
} }
# ─── Test domain using caddy-dns/rfc2136 our own CoreDNS plugin ──
#
# Proves the full self-hosted ACME DNS-01 loop end-to-end:
# 1. Caddy attempts to issue a cert for test-rfc2136.supported.systems
# 2. Caddy uses caddy-dns/rfc2136 to UPDATE _acme-challenge.<name>
# TXT record into supported.systems via our CoreDNS plugin
# 3. Plugin verifies TSIG, writes the zone file, bumps SOA
# 4. CoreDNS reloads (auto plugin, ~30s)
# 5. HE pulls the new serial within ~300s
# 6. Let's Encrypt validates from public DNS via HE
# 7. Caddy issues the cert
test-rfc2136.supported.systems {
tls {
dns rfc2136 {
key_name acme-update-key.
key_alg hmac-sha256
key {env.ACME_TSIG_SECRET}
# `coredns` resolves on the docker network to the CoreDNS
# container; port 53 is its in-container listener.
server coredns:53
}
# Use public resolvers (not docker's embedded DNS) for the
# post-update propagation check. Allow HE plenty of time to pull.
resolvers 1.1.1.1 9.9.9.9 1.0.0.1
propagation_delay 60s
propagation_timeout 600s
}
respond "test-rfc2136: cert issued via self-hosted RFC 2136 ACME flow!" 200
}

View File

@ -8,7 +8,8 @@ FROM caddy:2.10.0-builder AS builder
# a plugin's minimum Go version moves. # a plugin's minimum Go version moves.
ENV GOTOOLCHAIN=auto ENV GOTOOLCHAIN=auto
RUN xcaddy build \ RUN xcaddy build \
--with github.com/caddy-dns/vultr --with github.com/caddy-dns/vultr \
--with github.com/caddy-dns/rfc2136
FROM caddy:2.10.0 FROM caddy:2.10.0
COPY --from=builder /usr/bin/caddy /usr/bin/caddy COPY --from=builder /usr/bin/caddy /usr/bin/caddy

View File

@ -11,6 +11,9 @@ services:
environment: environment:
- CADDY_HOSTNAME=${CADDY_HOSTNAME} - CADDY_HOSTNAME=${CADDY_HOSTNAME}
- ACME_EMAIL=${ACME_EMAIL} - ACME_EMAIL=${ACME_EMAIL}
# Used by caddy-dns/rfc2136 for the test-rfc2136 site -- same
# secret CoreDNS's rfc2136 plugin verifies on the other side.
- ACME_TSIG_SECRET=${ACME_TSIG_SECRET}
# Optional: only required for Caddy's DNS-01 cert renewal via Vultr's # Optional: only required for Caddy's DNS-01 cert renewal via Vultr's
# API. Cert is valid ~90 days; this env var only matters within the # API. Cert is valid ~90 days; this env var only matters within the
# final 30d renewal window. Empty default keeps `docker compose up` # final 30d renewal window. Empty default keeps `docker compose up`