From cde2f2330c48ccdbb68a576f0d9bd88bcb9e94b7 Mon Sep 17 00:00:00 2001 From: Keith Jackson Date: Sat, 15 Aug 2026 23:02:59 -0500 Subject: [PATCH] Resolve OQ-4: scoped Cloudflare API token for DNS-01 + ExternalDNS Operator chose Option A. cert-manager solves ACME DNS-01 and ExternalDNS manages *.home.keithjackson.dev records via a scoped Cloudflare token (Zone.DNS.Edit, keithjackson.dev), stored SOPS-encrypted. Operator provides the token once (new prereq T-2.4p); T-2.4/T-2.5 unblocked. Only OQ-BKP (off-cluster DR backup target, Phase 4) remains open. Updated: docs 00,08,09; task board. Co-Authored-By: Claude Opus 4.8 (1M context) --- agents/task-board.md | 7 +++--- docs/00-vision-and-scope.md | 15 ++++++++----- docs/08-tls-and-certificates.md | 39 ++++++++++++++++----------------- docs/09-dns-and-ingress.md | 14 +++++------- 4 files changed, 37 insertions(+), 38 deletions(-) diff --git a/agents/task-board.md b/agents/task-board.md index 720224d..31fd681 100644 --- a/agents/task-board.md +++ b/agents/task-board.md @@ -39,8 +39,9 @@ Seeded from [`../roadmap/phases.md`](../roadmap/phases.md) + [`../roadmap/workst | T-2.1 | Longhorn (3-replica StorageClass) + `bulk` external-mount indirection (media lives off-cluster, backing TBD — OQ-2 resolved) | WS-STORAGE | R-STATE-02/04 | T-1.4 | — | todo | | T-2.2 | Default-deny network baseline (per-namespace) + Hubble | WS-NET | R-NET-01/03 | T-1.3 | — | todo | | T-2.3 | Render `dependencies` → CiliumNetworkPolicy from ServiceDefinition | WS-NET/WS-GITOPS | R-NET-02 | T-0.3, T-2.2 | — | todo | -| T-2.4 | cert-manager + Let's Encrypt DNS-01 wildcard `*.home.keithjackson.dev` (**blocked on OQ-4**: Cloudflare token vs acme-dns) | WS-SEC | R-TLS-01/02/03 | T-1.4, OQ-4 | — | blocked | -| T-2.5 | ExternalDNS + Gateway API listeners (VIP via **Cilium L2 announcement**) + split-horizon (PiHole) foundation | WS-NET | R-DNS-01/02/03 | T-2.4 | — | todo | +| T-2.4p | **Prereq:** operator creates scoped Cloudflare API token (`Zone·DNS·Edit`, `keithjackson.dev`); store SOPS-encrypted | WS-SEC | R-TLS-01, R-SEC-01 | — | — | todo | +| T-2.4 | cert-manager + Let's Encrypt DNS-01 wildcard `*.home.keithjackson.dev` (via Cloudflare token) | WS-SEC | R-TLS-01/02/03 | T-1.4, T-2.4p | — | todo | +| T-2.5 | ExternalDNS (Cloudflare token) + Gateway API listeners (VIP via **Cilium L2 announcement**) + split-horizon (PiHole) | WS-NET | R-DNS-01/02/03 | T-2.4 | — | todo | | T-2.6 | Prove: default-deny (DNS-only probe), PVC survives rebuild, trusted cert, auto-DNS | WS-NET/STORAGE/SEC | R-NET/STATE/TLS/DNS | T-2.1..2.5 | — | todo | | T-2.7 | Deploy **Kyverno in-cluster as admission enforcer** (same policies as the CI L0 gate) | WS-TEST/WS-SEC | R-SEC-02, R-NET-01, R-SEC-01 | T-1.4, T-0.5 | — | todo | @@ -100,5 +101,5 @@ Seeded from [`../roadmap/phases.md`](../roadmap/phases.md) + [`../roadmap/workst | OQ-1 | WAN count + edge router | — | ✅ resolved: 1 WAN, dumb SOHO edge; multi-WAN out of scope; VPN + segmentation in-cluster | | OQ-2 | Bulk-media home | — | ✅ resolved: media outside cluster, backing TBD later (not blocking) | | OQ-3 | VPN provider/protocol | — | ✅ resolved: ExpressVPN, single shared in-cluster tunnel, OpenVPN UDP (ADR-0012) | -| **OQ-4** | **DNS-01 challenge access** | **T-2.4, T-2.5** | ⛔ **OPEN** — Cloudflare API token (Option A, recommended) vs acme-dns delegation (Option B) | +| OQ-4 | DNS-01 challenge access | — | ✅ resolved: scoped **Cloudflare API token** (Zone·DNS·Edit), SOPS-encrypted; drives cert-manager + ExternalDNS. Operator provides token (T-2.4p) | | **OQ-BKP** | **Off-cluster DR backup target** (S3/NFS endpoint) | T-4.3 | ⛔ **OPEN** — needed by Phase 4, not Phase 2 | diff --git a/docs/00-vision-and-scope.md b/docs/00-vision-and-scope.md index fcef8d3..ab783c2 100644 --- a/docs/00-vision-and-scope.md +++ b/docs/00-vision-and-scope.md @@ -195,10 +195,13 @@ implementation. tunnel**, **non-TCP** (OpenVPN UDP / Lightway-UDP), rules enforced outside the service (ADR-0012). See [`03`](03-networking-isolation-and-egress.md). +- **OQ-4 (TLS/DNS challenge access)** — **RESOLVED**: a **scoped Cloudflare API token** + (`Zone·DNS·Edit`, `keithjackson.dev`), SOPS-encrypted, drives cert-manager DNS-01 and ExternalDNS. + The static `*.keithjackson.dev` wildcard covers reachability but not TXT-challenge writes; the token + handles issuance + record automation. Operator provides the token once. See + [`08`](08-tls-and-certificates.md). + ### Still open -- **OQ-4 (TLS/DNS challenge access)**: Cloudflare already has a wildcard `*.keithjackson.dev` → public - IP (covers reachability), but **ACME DNS-01 needs write access** to create `_acme-challenge` TXT - records, and `*.keithjackson.dev` does **not** match the two-label-deep `*.home.keithjackson.dev`. - Decide the challenge mechanism: **(a)** a scoped Cloudflare **API token** (Zone·DNS·Edit) → fully - automated DNS-01 + ExternalDNS, or **(b)** **acme-dns delegation** (one-time `_acme-challenge` - CNAME edit, then no standing Cloudflare credential). See [`08`](08-tls-and-certificates.md). +- **OQ-BKP (off-cluster DR backup target)**: which S3-compatible endpoint or NFS host stores + Velero/Longhorn backups of the irreplaceable tier. Needed by **Phase 4**, not blocking Phase 2. + See [`04`](04-storage-and-state.md). diff --git a/docs/08-tls-and-certificates.md b/docs/08-tls-and-certificates.md index f0fb252..7757c3b 100644 --- a/docs/08-tls-and-certificates.md +++ b/docs/08-tls-and-certificates.md @@ -28,24 +28,23 @@ services (R-TLS-01/02/03). needs no new cert), with per-service certs available where isolation warrants. - The DNS provider API token is a SOPS-encrypted secret ([07](07-security-and-secrets.md)). -## DNS-01 challenge access (OQ-4 — the one open item) +## DNS-01 challenge access (OQ-4 — RESOLVED: scoped Cloudflare API token) -Cloudflare already has a wildcard `*.keithjackson.dev` → public-IP record. **That covers -reachability but not certificate issuance**: ACME DNS-01 requires cert-manager to **write -`_acme-challenge` TXT records**, and the existing wildcard does not match two-label-deep names like -`plex.home.keithjackson.dev`. So a **challenge-access decision is still required**: +Cloudflare already has a wildcard `*.keithjackson.dev` → public-IP record, which covers +*reachability* but not certificate issuance (ACME DNS-01 must **write `_acme-challenge` TXT records**, +and that wildcard does not match two-label-deep names like `plex.home.keithjackson.dev`). -- **Option A — scoped Cloudflare API token** (Zone·DNS·Edit for `keithjackson.dev`), stored - SOPS-encrypted ([07](07-security-and-secrets.md)). cert-manager writes TXT challenges and - ExternalDNS manages the `*.home.keithjackson.dev` records — **fully automated**. *Recommended - (simplest).* -- **Option B — acme-dns delegation**: a one-time `_acme-challenge` **CNAME** in Cloudflare points to - an in-cluster acme-dns server; cert-manager writes only to acme-dns, so **no standing Cloudflare - credential** lives in the cluster. More moving parts; public `*.home` records then need the - one-time wildcard record instead of ExternalDNS. +**Decision (OQ-4):** use a **scoped Cloudflare API token** — `Zone·DNS·Edit` limited to the +`keithjackson.dev` zone — stored **SOPS-encrypted** ([07](07-security-and-secrets.md)). With it: +- **cert-manager** solves DNS-01 by writing/removing TXT challenge records automatically (issue, + renew, revoke — R-TLS-02), including the wildcard `*.home.keithjackson.dev` cert and internal-only + services (R-TLS-03). +- **ExternalDNS** manages the `*.home.keithjackson.dev` records automatically ([09](09-dns-and-ingress.md), + R-DNS-02) — no manual record editing for public services. -Either option supports the wildcard `*.home.keithjackson.dev` cert and internal-only issuance -(R-TLS-03). This is the remaining blocker for the TLS/DNS workstream (Phase 2). +**Operator prerequisite:** create the scoped token in Cloudflare and hand it over once; it is then +committed SOPS-encrypted and never touched manually again. The token is least-privilege (single zone, +DNS edit only) so its blast radius is bounded (R-SEC-02). Rotation = re-encrypt + commit. ## Renewal & revocation (R-TLS-02) @@ -75,8 +74,8 @@ CA distribution. Split-horizon DNS ([09](09-dns-and-ingress.md)) resolves - Certificate expiry and issuance failures raise alerts. - Onboarding a new service requires no manual certificate step. -## Open questions -- **OQ-4** — DNS-01 challenge access: **Option A (scoped Cloudflare API token, recommended)** vs - **Option B (acme-dns delegation)**. Provider is Cloudflare (confirmed). This is the only item still - blocking Phase 2 TLS. -- Wildcard vs. per-service certs default — **resolved: wildcard** `*.home.keithjackson.dev` (ADR-0006). +## Resolved +- **OQ-4** — DNS-01 via a **scoped Cloudflare API token** (`Zone·DNS·Edit`, `keithjackson.dev`), + SOPS-encrypted; drives both cert-manager DNS-01 and ExternalDNS. Phase 2 TLS/DNS is unblocked once + the operator provides the token. +- Wildcard vs. per-service certs default — **wildcard** `*.home.keithjackson.dev` (ADR-0006). diff --git a/docs/09-dns-and-ingress.md b/docs/09-dns-and-ingress.md index 040b215..d987f64 100644 --- a/docs/09-dns-and-ingress.md +++ b/docs/09-dns-and-ingress.md @@ -62,9 +62,9 @@ A wildcard `*.keithjackson.dev` matches only **one** label deep — it covers `h but **not** `plex.home.keithjackson.dev`. Consequences: - **`internal` services need no public record** — PiHole resolves `.home.keithjackson.dev` to the internal Gateway VIP (split-horizon). This is the majority of services. -- **`public` services** need a public record for `.home.keithjackson.dev`. That is provided by - either a one-time `*.home.keithjackson.dev` → public-IP record, or by **ExternalDNS via the DNS - API** — both hinge on OQ-4 (see [08](08-tls-and-certificates.md)). +- **`public` services** need a public record for `.home.keithjackson.dev`. Per OQ-4 (resolved), + **ExternalDNS manages these automatically** via the scoped Cloudflare API token — no manual record + editing (see [08](08-tls-and-certificates.md)). ## Interaction with default-deny @@ -81,9 +81,5 @@ not let other in-cluster services reach it unless an explicit allow rule exists. ## Resolved - **OQ-1** — single SOHO edge (manual port-forward only); ingress VIP via **Cilium L2 announcement**. - -## Open questions -- **OQ-4** — DNS challenge/record access: Cloudflare **API token** (automated ExternalDNS + DNS-01) - vs **acme-dns delegation** (one-time CNAME, no standing credential). Determines whether public - `*.home.keithjackson.dev` records and ACME challenges are automated or partly manual. See - [08](08-tls-and-certificates.md). +- **OQ-4** — scoped **Cloudflare API token** drives ExternalDNS (records) and cert-manager DNS-01 + (certs) automatically; see [08](08-tls-and-certificates.md).