diff --git a/.env.local.example b/.env.local.example new file mode 100644 index 0000000..6327076 --- /dev/null +++ b/.env.local.example @@ -0,0 +1,7 @@ +# Template for .env.local — copy to .env.local and fill in real values. +# .env.local is gitignored; this file documents what must be in it. + +# TSIG shared secret for rfc2136 plugin + caddy-dns/rfc2136. +# Generate with: openssl rand -base64 32 +# Rotate by regenerating + restarting CoreDNS + Caddy. +ACME_TSIG_SECRET= diff --git a/Corefile b/Corefile index feaf111..ab05857 100644 --- a/Corefile +++ b/Corefile @@ -57,3 +57,29 @@ https://.:443 { tls /etc/coredns/certs/cert.pem /etc/coredns/certs/key.pem import common } + +# ─── PHASE 0 SCAFFOLDING — NOT YET ACTIVE ────────────────────────── +# Dynamic-update server for ACME DNS-01 challenges (RFC 2136 + TSIG). +# Caddy uses caddy-dns/rfc2136 to push TSIG-signed UPDATE messages here; +# the plugin stores TXT records in memory and serves them for Let's +# Encrypt's validation queries. +# +# Activation requires: +# 1. The coredns-rfc2136 plugin built into a custom CoreDNS image +# (see coredns/Dockerfile and docker-compose.yml build directive). +# 2. ACME_TSIG_SECRET set in .env.local (already generated). +# 3. zones/supported.systems.zone delegating `auth` sub-zone to dell01: +# auth 300 IN NS dns.supported.systems. +# 4. FortiWiFi firewall opening UDP/53 to dell01 from 0.0.0.0/0. +# +# Until those land, this block is a comment. The plan lives at +# ~/.claude/plans/dood-does-coredns-offer-enumerated-piglet.md +# +# .:53 auth.supported.systems { +# rfc2136 auth.supported.systems { +# tsig-key acme-update-key. hmac-sha256 {$ACME_TSIG_SECRET} +# ttl 60 +# } +# errors +# log +# } diff --git a/coredns/Dockerfile b/coredns/Dockerfile new file mode 100644 index 0000000..3df04c6 --- /dev/null +++ b/coredns/Dockerfile @@ -0,0 +1,41 @@ +# Custom CoreDNS image that bakes in the rfc2136 plugin for accepting +# RFC 2136 dynamic updates (TSIG-authenticated). The upstream +# coredns/coredns image does NOT include this plugin — CoreDNS itself +# has no plugin for accepting dynamic updates anywhere in its ecosystem +# as of v1.12.2, so we ship our own. +# +# Stage 1: build CoreDNS from source with our plugin appended to +# plugin.cfg. Stage 2: distroless runtime image. +# +# Plugin source: +# This Dockerfile is currently SCAFFOLDING ONLY — the plugin repo does +# not yet exist. Building this image will fail until Phase 1 ships. + +# ─── Stage 1: builder ────────────────────────────────────────────── +FROM golang:1.22-alpine AS builder + +RUN apk add --no-cache git make + +WORKDIR /build +ARG COREDNS_REF=v1.12.2 +RUN git clone --depth 1 --branch ${COREDNS_REF} https://github.com/coredns/coredns.git . + +# Inject our plugin into plugin.cfg. Must come BEFORE the `cache` plugin +# so authoritative answers from rfc2136 aren't intercepted by cache. +ARG PLUGIN_REPO=git.supportedsystems.net/rpm/coredns-rfc2136 +ARG PLUGIN_REF=latest +RUN sed -i "/^cache:cache$/i rfc2136:${PLUGIN_REPO}" plugin.cfg && \ + go get ${PLUGIN_REPO}@${PLUGIN_REF} + +RUN make GOFLAGS="-ldflags=-w -s" + +# ─── Stage 2: runtime ────────────────────────────────────────────── +FROM gcr.io/distroless/static-debian12 + +COPY --from=builder /build/coredns /coredns + +# Match upstream's exposed ports. +EXPOSE 53 53/udp 853 443 9153 8080 + +ENTRYPOINT ["/coredns"] +CMD ["-conf", "/etc/coredns/Corefile"] diff --git a/scripts/acme-add-domain.sh b/scripts/acme-add-domain.sh new file mode 100755 index 0000000..4a65200 --- /dev/null +++ b/scripts/acme-add-domain.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# Bootstraps a domain for self-hosted ACME DNS-01 cert automation. +# +# Adds a single `_acme-challenge. CNAME .auth.supported.systems` +# record to the zone file. After this one-time edit + push, all future cert +# issuance and renewal for the domain happens via dynamic RFC 2136 UPDATEs +# to the auth.supported.systems sub-zone (served by CoreDNS + rfc2136 plugin +# on dell01) — no further zone-file churn. +# +# Usage: +# scripts/acme-add-domain.sh example.com +# +# After running: +# 1. Verify the line was added correctly: `tail -3 zones/example.com.zone` +# 2. Commit: `git add zones/example.com.zone` +# 3. Push: rsync to dell01, `make prep` +# 4. Configure Caddy with the same UUID (see plan Phase 6). +# +# This script is SCAFFOLDING — the upstream rfc2136 plugin and the +# auth.supported.systems delegation must be operational before the +# generated CNAMEs actually do anything useful. + +set -euo pipefail + +DOMAIN="${1:?usage: $(basename "$0") }" +ZONE_FILE="zones/${DOMAIN}.zone" + +# Must run from the repo root so the relative zone path resolves. +if [[ ! -f "$ZONE_FILE" ]]; then + echo "Zone file not found: $ZONE_FILE" >&2 + echo "Run from the coredns repo root, and ensure the zone exists." >&2 + exit 1 +fi + +# Refuse to add a duplicate. +if grep -qE "^_acme-challenge\b" "$ZONE_FILE"; then + echo "_acme-challenge record already present in $ZONE_FILE — skipping." >&2 + echo "Existing line(s):" >&2 + grep -E "^_acme-challenge\b" "$ZONE_FILE" >&2 + exit 1 +fi + +UUID="$(cat /proc/sys/kernel/random/uuid)" +LINE=$(printf "_acme-challenge\t300\tIN\tCNAME\t%s.auth.supported.systems" "$UUID") + +echo "$LINE" >> "$ZONE_FILE" + +echo "Added to $ZONE_FILE:" +echo " $LINE" +echo "" +echo "Caddyfile snippet for ${DOMAIN}:" +cat <