Files
home-network-k3s/docs/08-tls-and-certificates.md
T
jakman85andClaude Opus 5.5 18f7ba8d0f docs: apply the operator's rulings on Kasm, revocation and fault injection
Retire Kasm from the catalog. Drop ACME revocation from R-TLS-02. Make L3
fault injection operator-run and exclude it from the merge gate (R-TEST-03,
ADR-0026). Update the gap register to match; Homepage authentication is now
ruled must-enforce and stays a gap until the implementation lands.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 23:06:26 -06:00

92 lines
5.4 KiB
Markdown

# 08 — TLS & Certificates
Serves: **R-TLS-01/02/03**, **R-DNS-01**. Related: [ADR-0006 (cert-manager + Let's Encrypt)](../decisions/adr-0006-tls-cert-manager-letsencrypt.md),
[ADR-0016 (Cloudflare Tunnel public ingress)](../decisions/adr-0016-public-ingress-cloudflare-tunnel.md).
## Requirement recap
Every externally reachable service presents TLS from a **credible, trusted, non-self-signed**
authority, with certificates **auto issued and renewed** — including for **internal-only**
services (R-TLS-01/02/03).
## Solution: cert-manager + Let's Encrypt via DNS-01
- **cert-manager** runs in the cluster and manages the full certificate lifecycle.
- **Let's Encrypt** is the trusted, non-self-signed CA (publicly trusted; free; automatable) —
satisfies R-TLS-01.
- **ACME DNS-01 challenge** against the `keithjackson.dev` DNS zone (Cloudflare or equivalent, per
OQ-4). DNS-01 is the key choice because it validates domain control **without exposing the service
publicly** — so internal-only services get real, trusted certs (R-TLS-03), and **wildcard**
certificates (`*.home.keithjackson.dev`) are possible.
## Issuance model
- A `ClusterIssuer` for Let's Encrypt production (and one for staging, used during bring-up to avoid
rate limits).
- Certificates are requested declaratively — either a **wildcard** `*.home.keithjackson.dev` cert
shared by the Gateway, or **per-service** certs, decided per ADR-0006. Recommendation: a wildcard
cert for `*.home.keithjackson.dev` terminated at the Gateway simplifies onboarding (a new service
needs no new cert), with per-service certs available where isolation warrants (Plex's L4 VIP name
has its own).
- The DNS provider API token is a SOPS-encrypted secret ([07](07-security-and-secrets.md)).
## DNS-01 challenge access (OQ-4 — RESOLVED: scoped Cloudflare API token)
Certificate issuance needs programmatic write access to the zone: ACME DNS-01 must **write
`_acme-challenge` TXT records**, including for two-label-deep names like
`plex.home.keithjackson.dev`.
**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 — 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.
- **cloudflare-ddns** keeps the DNS-only `wan.` record current for WireGuard ([09](09-dns-and-ingress.md)).
**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 (R-TLS-02)
- cert-manager renews well before expiry automatically; no human action.
- Removing or rotating a certificate is a declarative change; cert-manager issues the replacement.
The platform does not perform ACME revocation: a compromised key is handled by rotation. Expiry monitoring is an alert condition ([10](10-observability-and-alerting.md), R-OBS-04).
## Where TLS terminates
- **North-south**: at the **Gateway API** listener (implemented by Cilium, [09](09-dns-and-ingress.md)),
using the cert-manager-issued cert. Internal services resolve to the Gateway on the LAN.
- **Public services via the Cloudflare Tunnel (ADR-0016)** are TLS twice:
- **Edge:** Cloudflare terminates the client's TLS with an ACM/Total TLS certificate. It is
publicly trusted and auto-renewed (R-TLS-01/02). Universal SSL is not enough, because it covers
only one subdomain level.
- **Origin:** cloudflared re-encrypts to the Gateway and **validates** the Let's Encrypt wildcard,
using the service hostname as SNI. Verification is never disabled.
Cloudflare sees plaintext between the two legs, a trade-off accepted in ADR-0016.
- Internal service-to-service traffic relies primarily on network policy for security; mTLS between
workloads is **optional** future hardening (could be added via Cilium/service mesh) and is not a
launch requirement — record as a possible future ADR.
## Trust chain for internal clients
Both certificates a client can meet are publicly trusted, so internal clients need no custom CA
distribution. A name Pi-hole holds a local record for ([09](09-dns-and-ingress.md)) resolves to the
internal Gateway and serves the Let's Encrypt wildcard; any other `public` name resolves to
Cloudflare's edge and serves its edge certificate.
## Acceptance gates
- `https://<service>.home.keithjackson.dev` serves a valid, non-self-signed, unexpired certificate
(verified by an external TLS checker), including for internal-only services.
- Certificates renew automatically ahead of expiry with no manual step (observed across a renewal).
- Certificate expiry and issuance failures raise alerts.
- Onboarding a new service requires no manual certificate step.
## Resolved
- **OQ-4** — DNS-01 via a **scoped Cloudflare API token** (`Zone·DNS·Edit`, `keithjackson.dev`),
SOPS-encrypted; drives cert-manager DNS-01, ExternalDNS and the `wan.` record updater.
- Wildcard vs. per-service certs default — **wildcard** `*.home.keithjackson.dev` (ADR-0006).