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]>
Record the operator's 2026-11-08 decision to move public HTTP(S) ingress
from edge port-forwarding of 80/443 to an outbound-only Cloudflare Tunnel
(in-cluster cloudflared, proxied CNAMEs via ExternalDNS). Motivation: the
GFiber router cannot hairpin NAT and advertises Google IPv6 DNS via RA, so
LAN clients bypass Pi-hole split horizon and fail.
Docs 00/01/03/07/08/09/13/14, GLOSSARY and definition-of-done brought into
line. R-DNS-01..03 and R-TLS-01..03 wording unchanged. New requirements:
R-DNS-04 (no 80/443 forward, resolver-independent LAN reachability),
R-DNS-05 (only `public` services at the tunnel; 404 catch-all; no public
record for `internal`), R-SEC-04 (Kubernetes API never publicly reachable).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Make the operator's requirement explicit: default (every pod + the nodes) egresses
the local/private WAN, never the VPN; VPN is strictly opt-in via egress-class=vpn;
the kill-switch blackholes ONLY vpn-class. The two classes fail in opposite
directions — internet fails open (WAN), vpn fails closed (blackhole). Added as ADR-0015
Invariants + a default-WAN principle in docs/03, gated by the egress-class/vpn-killswitch
conformance tests.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The in-cluster Gluetun-pod gateway (ADR-0013) cannot be steered: a pod's tun0 is
invisible to the node datapath, and hostNetwork Gluetun hijacks the node's own
default route (breaks k3s/etcd). The tunnel itself works (PIA egress validated),
but not the steering.
New decision (ADR-0015): move the tunnel to a HOST OpenVPN systemd service on
egress-gateway node(s) (Ansible-managed, split-tunnel so it doesn't touch the node
default route), steer egress-class=vpn pods with a CiliumEgressGatewayPolicy (native
eBPF, no client-pod changes), and enforce the kill-switch with a host nftables rule
(fail-closed independent of Cilium). PIA/OpenVPN-UDP provider decision (ADR-0013)
stands; the .ovpn transfers over; PF re-solved host-side (Phase 6).
Amended for consistency: ADR-0013 (client+steering superseded), decisions/README,
docs/00, docs/01, docs/03, docs/07, agents/task-board.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Gluetun does not support Private Internet Access over WireGuard (upstream:
native support "cannot be added... slow work in progress"; confirmed live
rejecting the provider on v3.40/v3.41/latest). PIA in Gluetun is OpenVPN-only.
Amend ADR-0013 to PIA over OpenVPN-UDP. The egress ARCHITECTURE is unchanged
(single shared gateway, central kill-switch/fail-closed, native PF, Cilium
egress steering, active/standby) and R-NET-05/06/07 remain met; only the tunnel
protocol changes. OpenVPN-UDP satisfies "non-TCP", and Gluetun's HTTP control
API (health + forwarded-port lookup) is what the shared-gateway topology needs.
thrnz/docker-wireguard-pia retained as a documented WireGuard fallback.
Propagated the protocol correction to all active specs: ADR-0013 (+amendment
note), decisions/README, docs/00, docs/01, docs/03, docs/07, agents/task-board.
Superseded ADR-0008/0012 left immutable as historical record.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
New identity subsystem, and resolves the C2 auth open question:
- docs/15-identity-and-access.md + ADR-0014: Keycloak as central IdP; native
OIDC where supported (Gitea, Nextcloud, Grafana, Kasm, Semaphore, Headlamp)
and oauth2-proxy forward-auth for bare UIs (PiHole, *arr, qBittorrent,
Homepage, Prometheus, Alertmanager, Hubble). Group-based authz + MFA;
declarative realm/clients (GitOps); PostgreSQL on Longhorn, DR-critical +
break-glass; hardened public exposure. Plex/Seerr use Plex auth; Minecraft N/A.
- Requirements R-IDP-01..06; ServiceDefinition gains an block.
- Onboarded early (with Gitea) so services adopt SSO as they come up.
- Threaded through: docs 00,01,07,10,12,13; roadmap phases+workstreams;
task board (T-6.1c/T-6.1d); glossary; ADR index; README.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Operator turned off ExpressVPN auto-renew and purchased PIA. The single-shared-
gateway model is retained; provider/client/protocol change to PIA via Gluetun
over WireGuard with native port forwarding (qbittorrent-natmap port-sync).
Simpler than ExpressVPN: no custom OpenVPN config, just PIA user/pass
SOPS-encrypted; Gluetun provides the built-in kill-switch.
Updated: ADR-0013 (new) + ADR-0012 superseded + index; docs 00,01,03,07;
task board (T-3.2p/T-3.2, OQ-3).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
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) <[email protected]>
Operator answers folded into the specs:
- OQ-3 VPN: ExpressVPN, cluster-controlled SINGLE shared in-cluster tunnel
(OpenVPN UDP / non-TCP), rules enforced outside the service. New ADR-0012
supersedes ADR-0008 (Gluetun-sidecar default dropped). Central kill-switch.
- OQ-1 WAN/edge: one commercial WAN, no multi-ISP plans; dumb SOHO edge (no
VLAN/VPN/services). Multi-WAN out of scope; segmentation + VPN in-cluster;
edge = NAT + manual port-forward. Gateway VIP resolved to Cilium L2
announcement. T-3.3 marked won't-do.
- OQ-2 storage: bulk media lives outside the cluster (backing TBD, non-blocking)
via a 'bulk' external-mount indirection; Plex metadata DB stays on Longhorn.
Split out a separate OQ-BKP for the off-cluster DR backup target (Phase 4).
- OQ-4 refined: wildcard record covers reachability but NOT ACME DNS-01 (needs
TXT-write access) and does not match *.home.* depth. Still OPEN: Cloudflare
API token (Option A) vs acme-dns delegation (Option B). T-2.4/T-2.5 blocked.
Updated: docs 00,01,03,04,08,09; ADR-0008/0012 + index; task board.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>