The operator ruled the implementation repo's main (50c8719) the latest and accepted state. Sync the spec to it. Accepted changes are reflected: one bare-metal environment, TrueNAS bulk storage, R2 off-cluster backup, CloudNativePG, single-node VPN gateway with Cilium fail-closed, inbound WireGuard, per-app auth sidecars, Renovate, the CI gate as approver, and the pm-agent. New ADR-0017..0025; ADR-0016 corrected and accepted; R-* IDs unchanged, eight amended. Where the implementation has simply not met a requirement, the requirement stands and the shortfall is recorded in docs/16-known-gaps.md. Co-Authored-By: Claude Opus 5.5 <[email protected]>
15 KiB
00 — Vision & Scope
Vision
Build a home platform that behaves like a small private cloud. An operator declares a service — a container app or a virtual machine — in git, and the platform provisions it, secures it, exposes it under a trusted TLS URL, routes its traffic on the correct WAN, protects its data, updates it automatically, and reports on its health. Onboarding a new service is a pull request against a catalog, not a weekend of manual work.
The platform runs as one live environment: prod, on 3 bare-metal mini-PCs (ADR-0017). The dev/test cluster that proved its viability was decommissioned in 2026-09; every change reaches prod through the PR gate and Flux.
Personas
- Operator (primary user): runs the home network; wants to onboard/offboard services with minimal ceremony and zero data loss.
- Platform (the system): reconciles declared state, enforces isolation and egress policy, renews certificates, updates workloads, and raises alerts.
- Implementation agents: the AI teams building the platform from this spec.
In scope
- A k3s-based platform with distributed storage (Longhorn) across 3 nodes.
- Container workloads (PaaS) and VM workloads (IaaS) through one Kubernetes surface.
- GitOps delivery, automated updates, trusted TLS, DNS, ingress.
- Network isolation (default-deny east-west) and declared egress classes, including VPN routing.
- Observability with email + dashboard alerting.
- A command-and-control web UI at the root of
home.keithjackson.dev. - The service catalog (see
13-service-catalog.md).
Out of scope (for now)
- Multi-tenant / multi-operator RBAC beyond a single operator + platform automation.
- Public multi-user SaaS concerns (billing, per-tenant isolation beyond network policy).
- Non-
keithjackson.devdomains.
The domain & DNS model
- Public apex
*.keithjackson.devis available in Cloudflare DNS.publicservices (catalogexposure: publicor a registered platform route) under*.home.keithjackson.devare published through a Cloudflare Tunnel (proxied records, no inbound 80/443 at the edge — ADR-0016); the network's public IP stays in use for WireGuard and Plex only. - All platform services live under
*.home.keithjackson.dev. - Each service is reachable at
<service>.home.keithjackson.dev(e.g.plex.home.keithjackson.dev). - The root
home.keithjackson.devserves the Command & Control (C2) center UI.
Requirements (stable IDs)
Requirement IDs are stable references used across ADRs, specs, the task board, and PRs. MUST is
mandatory; SHOULD is strongly preferred and requires an ADR to deviate.
Platform & API surface — R-PLAT-*
- R-PLAT-01 — The platform MUST run on k3s across 3 nodes with production-grade HA control plane in prod.
- R-PLAT-02 — Container workloads MUST be onboarded and managed declaratively via the Kubernetes
API,
kubectl-class CLI, and YAML. - R-PLAT-03 — Virtual-machine workloads MUST be created, managed, and destroyed through the same Kubernetes API/CLI/YAML surface as containers (no separate hypervisor management plane for application VMs).
- R-PLAT-04 — The platform MUST support create / destroy / rebuild of any workload on demand.
GitOps & delivery — R-GITOPS-*
- R-GITOPS-01 — All application and platform desired state MUST be defined in git and reconciled automatically (pull-based GitOps).
- R-GITOPS-02 — Onboarding a new service MUST be expressible as a change to a versioned catalog in git.
- R-GITOPS-03 — The system MUST detect and correct drift between git and cluster state.
Networking, isolation & egress — R-NET-*
- R-NET-01 — East-west traffic between services MUST be denied by default; communication is allowed only by explicit, port-level configuration (firewall-like).
- R-NET-02 — Allow rules MUST be declarative, reviewable in git, and scoped per source → destination → port/protocol.
- R-NET-03 — Networks MUST be configurable and isolatable (segments/namespaces with enforced boundaries).
- R-NET-04 — The platform MUST support multiple egress paths selected by routing rules. Per
OQ-1 the environment has one physical commercial WAN and no multi-ISP plans, so this is
realized as two paths — the local
internetpath and the in-clustervpnpath — not multi-ISP routing. Physical multi-WAN (multiple ISP uplinks) is out of scope unless a future ADR reopens it. - R-NET-05 — A service MUST be able to declare its egress class:
internet(standard local network/internet) orvpn(routed through a custom VPN connection). VPN routing MUST be enforced by a single shared node-level gateway outside the service (ADR-0015, ADR-0020), so a service cannot leak by misconfiguration (OQ-3). - R-NET-06 — VPN-class egress MUST enforce a kill-switch: if the VPN tunnel is down, the service MUST NOT leak traffic to the standard internet.
- R-NET-07 — Egress class MUST be a declarative property of the service, changeable via GitOps without re-architecting the workload.
Storage & state — R-STATE-*
- R-STATE-01 — Stateful data MUST survive destroy/rebuild of the workload (PaaS and IaaS).
- R-STATE-02 — Persistent storage MUST be distributed/replicated across the 3 nodes (Longhorn) so a single node loss does not lose data.
- R-STATE-03 — The platform MUST provide scheduled backups to an off-cluster target and a tested restore path (disaster recovery).
- R-STATE-04 — Bulk media storage (Plex library) MUST have a capacity strategy distinct from replicated app-config storage (replicating multi-TB media 3× is out of scope; see ADR-0003).
Automated updates — R-UPD-*
- R-UPD-01 — Services MUST update to the latest stable versions on a schedule.
- R-UPD-02 — Updates in the allowed classes MUST occur with no human, no AI, and no hand-authored GitOps change; automation acting on declarative policy opens machine-authored changes through the merge gate. Major versions and operator-designated blast-radius components MAY wait for human review of the machine-authored change.
- R-UPD-03 — Update policy MUST be per-service configurable (cadence, stability channel, pin/hold, auto-rollback on failed health).
TLS & certificates — R-TLS-*
- R-TLS-01 — Every externally reachable service MUST present TLS from a credible, trusted, non-self-signed authority.
- R-TLS-02 — Certificates MUST be automatically issued, renewed, and revoked without manual steps.
- R-TLS-03 — Certificate issuance MUST work for internal-only services (no requirement to expose a service publicly just to validate a certificate).
DNS & ingress — R-DNS-*
- R-DNS-01 — Services MUST be reachable at
<service>.home.keithjackson.dev. - R-DNS-02 — DNS records MUST be managed automatically from declared service intent.
- R-DNS-03 — Internal clients MUST resolve service names correctly (split-horizon where public and internal answers differ).
- R-DNS-04 —
publicHTTP(S) services MUST be reachable from the internet without inbound 80/443 port-forwarding at the edge, and LAN clients MUST reach them correctly whichever resolver answers (public or internal) — no dependency on NAT hairpin (ADR-0016). - R-DNS-05 — Only services declared public (catalog
exposure: publicor a registered platform route) MAY be reachable through the public ingress path. A request for a LAN/VPN-only hostname at that path MUST be refused (HTTP 404).internalservices MUST have no public record, except non-HTTP L4 names, which MAY hold DNS-only records at LAN addresses (ADR-0016).
Observability & alerting — R-OBS-*
- R-OBS-01 — Services MUST be monitored (metrics, logs, health).
- R-OBS-02 — Issues MUST be reported by email.
- R-OBS-03 — Issues and health MUST be visible on dashboards.
- R-OBS-04 — The platform MUST alert on its own health (nodes, storage, GitOps reconciliation, certificate expiry, VPN tunnel state, backup success).
Command & control — R-C2-*
- R-C2-01 — The root
home.keithjackson.devMUST serve a UI that surfaces the services running in the cluster and links to them. - R-C2-02 — The C2 UI MUST surface platform health (cluster, storage, GitOps, alerts) and provide controlled operational actions where safe.
Security & secrets — R-SEC-*
- R-SEC-01 — Secrets MUST NOT be stored in plaintext in git; they MUST be encrypted at rest in the repo and decrypted only in-cluster.
- R-SEC-02 — The platform MUST follow least-privilege for workload identities and network reach.
- R-SEC-03 — All the above properties MUST be demonstrable/auditable (an operator can prove isolation, egress path, cert validity, and backup integrity).
- R-SEC-04 — The Kubernetes API MUST NOT be reachable from the public internet by any path (no tunnel route, no edge forward, no public DNS record); it is LAN/VPN-only (ADR-0016).
Identity & access (SSO) — R-IDP-*
- R-IDP-01 — The platform MUST provide a central Identity Provider (SSO) as the single user directory and login for platform services.
- R-IDP-02 — Services MUST authenticate against the IdP — natively via OIDC where supported, or
behind an identity-aware (forward-auth) proxy otherwise. No management UI may be exposed
unauthenticated on the network. (Documented exceptions: Plex/Seerr use Plex auth; Minecraft uses
Mojang/Microsoft; UniFi uses its native login on its own L4 VIP; the read-only runbooks at
/runbookscarry no login; Crafty web uses forward-auth plus its own local login, no OIDC.) - R-IDP-03 — Identity configuration (realms, clients, groups, roles) MUST be declarative and GitOps-managed, not console-only.
- R-IDP-04 — IdP secrets (admin credential, OIDC client secrets) MUST be SOPS-encrypted; MFA MUST be available to all users and required for privileged access.
- R-IDP-05 — The IdP MUST be treated as DR-critical (replicated + off-cluster-backed datastore) with a documented break-glass path so an IdP outage does not lock the operator out of recovery tools.
- R-IDP-06 — Access MUST be least-privilege via groups/roles; sensitive surfaces (C2, cluster console, alerting) require an authenticated user in an authorized group, not merely "logged in."
Environments — R-ENV-*
- R-ENV-01 — Nodes MUST be baselined, cluster-joined and managed as code; machine creation on bare metal is an imaged, checklist-driven step.
- R-ENV-02 — GitOps configuration MUST be authored once as a base plus environment-scoped overlay; any future environment MUST need overrides only.
- R-ENV-03 — Production runs on 3× mini-PC bare metal. Changes MUST reach it only through the PR gate and Flux.
Conformance testing & governance — R-TEST-*
- R-TEST-01 — Every requirement MUST be verified by automated, behavior-based tests (Given/When/Then style) that assert observable system behavior, not implementation internals.
- R-TEST-02 — Tests MUST be reproducible: deterministic, hermetic (create and tear down their own fixtures), version-pinned, and runnable by a single command locally and in CI against any conforming cluster, producing machine-readable, requirement-tagged results.
- R-TEST-03 — A conformance suite MUST gate delivery: merges are blocked unless the tests for the touched requirements pass, and phase exits are blocked unless the phase's full suite passes. "Demonstrated" means "the automated test passes," never a manual one-off.
- R-TEST-04 — Tests MUST be traceable: each test declares the requirement ID(s) it verifies,
and a coverage report maps every
R-*requirement to its verifying test(s). Uncovered requirements are a gap to close, not an acceptable state. - R-TEST-05 — Safety-critical behaviors (default-deny, VPN kill-switch, data-survival on rebuild, DR restore, auto-rollback) MUST be verified by fault-injection behavior tests that prove fail-closed / no-data-loss outcomes, not merely happy-path checks.
- R-TEST-06 (governance) — AI agents MUST NOT modify the specifications (this
docs/tree, the requirements, or accepted ADRs). Specs are changed only by the human operator/architect. Agents that find a spec wrong, blocking, ambiguous, or missing MUST stop and escalate and MAY draft a proposal, but MUST NOT self-approve or merge a spec/ADR change (seeAGENTS.md§2 and §11).
Acceptance philosophy
Each requirement class maps to an automated, reproducible, behavior-based conformance test
(R-TEST-*). "Done" means the conformance test for the touched requirements passes — a
green, reproducible result traced to requirement IDs — never "manifest merged" and never a manual
demonstration. The testing approach is specified in
docs/14-testing-and-conformance.md and
ADR-0010; the gate checklist lives in
agents/definition-of-done.md.
Governance: the specifications are human-owned and immutable to agents (R-TEST-06). Agents implement against the spec and prove conformance with tests; they never edit the spec to fit the implementation.
Open questions
Resolved
-
OQ-1 (WAN / edge) — RESOLVED: one commercial WAN, no multi-ISP plans; dumb SOHO edge (no VLAN/VPN/services). All segmentation + VPN in-cluster; edge does NAT + port-forward only. Physical multi-WAN out of scope. See
03,09. Amended 2026-11-08 (ADR-0016): public web ingress moved to an outbound-only Cloudflare Tunnel; the edge no longer forwards 80/443. Its remaining forwards are WireGuard udp/51820 and Plex tcp/32400. -
OQ-2 (bulk media) — RESOLVED (ADR-0018): Plex bulk media lives outside the cluster on a standalone TrueNAS NFS server on the production LAN, and sits outside platform backup. Plex's metadata DB still lives on replicated Longhorn. See
04. -
OQ-3 (VPN) — RESOLVED: PIA over OpenVPN-UDP (non-TCP; ADR-0013). Egress is a node-level host tunnel steered by Cilium Egress Gateway, failing closed under Cilium's own enforcement (ADR-0015, ADR-0020) —
vpn-class pods routed through it, rules enforced outside the service. See03. -
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.devwildcard covers reachability but not TXT-challenge writes; the token handles issuance + record automation. Operator provides the token once. See08. -
OQ-BKP (off-cluster DR backup target) — RESOLVED (ADR-0018): Cloudflare R2 stores Velero backups (kopia file-system volume data) and CNPG Barman archives of the irreplaceable tier; Longhorn has no backup target of its own. See
04.
Still open
None tracked here. Unmet requirements are recorded in known gaps.