Prep only — nothing deployed; running cluster untouched. Reconciliation graph (operator-approved Option A): author clusters/dev/rollout/ = 4 per-component Flux Kustomizations (kyverno->cilium->longhorn->monitoring, monitoring dependsOn longhorn). Dev overlay $patch:delete's the monolithic base platform/apps and adds ./rollout, so only the no-secret tier reconciles; the secret-blocked components are simply not created until hand-offs arrive. The no-secret tier carries no SOPS content, so it needs no sops-age key. prod is untouched (still builds the monolith). Convergence path coded in (delete rollout/ + the two deletes) — R-ENV-02 divergence flagged for architect review. Component hardening via cluster-vars (prod keeps strict posture): - KYVERNO_VALIDATION_ACTION Enforce->Audit(dev), _WEBHOOK_FAILURE_POLICY Fail->Ignore(dev), _ADMISSION_REPLICAS 3->1(dev); webhook namespaceSelector excludes system+platform namespaces. - MONITORING_STORAGE_CLASS var; GRAFANA_ADMIN_SECRET var (dev ''=chart default); Alertmanager SMTP/secret deferred to Phase-4 with verbatim re-enable markers. Cilium parity verified against the live CNI (no-op adopt). Validated: kubectl kustomize dev/prod/all-components, envsubst->YAML parse, make -C ci gate PASS. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
15 KiB
Autonomous Session Decision Log (PM)
Program Manager working independently while the operator is AFK (started 2026-08-16). Every
best-effort decision and assumption is recorded here for later review. Specs were NOT modified
(PM cannot change ../home-network-k3s; that requires the System Architect hat). Blocked items that
need the operator are called out.
Guardrails I'm holding to
- No spec changes. No fabricated secrets (Cloudflare token, PIA creds, SMTP — all await hand-off).
- Don't break the running cluster:
platform/stays suspended in Flux until each component is filled in + validated;platform/ciliummust carryk8sServiceHost=10.73.0.11before it ever reconciles. - Everything author-first,
make -C ci gategreen before merge, feature-branch→main.
Decisions / assumptions (append-only)
| # | Decision / assumption | Why | Review flag |
|---|---|---|---|
| A1 | Complete Phase 1 T-1.4 via Option A (Flux bootstrapped, platform+apps suspended) |
You picked Option A earlier as the safe path; "keep going" authorizes it | Confirm scope |
| A2 | Advance Phase 2 only for components needing no operator secret: Longhorn, monitoring, Kyverno-admission, Cilium-parity | cert-manager/externaldns/velero/VPN are blocked on token/PIA/OQ-BKP | — |
| A3 | Dev resource caps for monitoring (short retention, small PVCs, single replicas) from cluster-vars |
22 GiB RAM ceiling | Tune later |
| A4 | Host Gitea repo is public (NAT-only) so Flux needs no git creds | dev bootstrap simplicity | Fine for dev |
Autonomous session results (2026-08-16)
- Phase-2 authoring (author-only, merged):
platform/longhorn(3-replica SC + bulk indirection),platform/monitoring(kube-prometheus-stack + Loki + Alloy, dev-trimmed ~1.3 GiB, +8 alert rules),platform/cilium(parity fix: k8sServiceHost=10.73.0.11 + masquerade, safe to adopt in Phase 2),platform/kyverno(verified). Gate green. - Flux INSTALLED on the cluster (4 controllers Running). Host Gitea working (named-volume fix),
repo pushed. GitRepository created → BLOCKED: cluster pods can't reach
10.73.0.1:3000(firewalldlibvirtzone drops VM→host:3000).- RESOLVED (operator added the ansible-vault become password): ran
host-prereqs.yml(firewall opened via vault→become), VM→Gitea = 200, and Flux GitRepository is READY=True (main@sha1:a2f3bdc…). Phase-1 T-1.4 core is DONE — Flux installed + reconciling clusters/dev from the host Gitea.platform/appsKustomizations intentionally NOT created (Option A) — the running cluster is untouched. - Firewall + Gitea admin/repo/push + host become password are all IaC now (host-prereqs.yml, bootstrap-gitea.sh automation, vaulted group_vars/hypervisor) → destroy/rebuild is hands-off.
- RESOLVED (operator added the ansible-vault become password): ran
- NOT deployed:
platform/appsKustomizations were deliberately NOT created — the running cluster (Cilium etc.) is untouched. Only Flux controllers + the GitRepository source exist.
T-1.5 — Destroy/rebuild proof (destructive IaC-validity test) — 2026-08-16
Goal (operator-scoped): prove the IaC is valid — the dev cluster can be destroyed and rebuilt from IaC + git with no data loss ("Approach A is acceptable"; "more about validity of the IaC than a COOP/DR scenario"). Ran after K2 conformance passed, with the operator approving each destructive step at the prompt.
Outcome: PASS (with one operator-visible scare and two concrete IaC findings). The cluster was torn down and reconstituted from IaC + preserved Longhorn/OS disks + the Flux git source. End state: 3/3 Ready (k3s-dev-1/2/3, control-plane,etcd,master), Cilium status OK, Flux GitRepository READY=True, zero state lost. Behavioral health re-verified post-rebuild: conformance L2 R-NET-01 default-deny + R-NET-02 deny-undeclared both PASS (exit 0, JUnit+coverage regenerated). The destroy/rebuild path is demonstrably valid.
Finding F1 — tofu destroy -target on a count-indexed resource is unsafe (ROOT CAUSE)
tofu destroy -target 'libvirt_domain.node[2]' (+ that node's os[2]/data[2]/seed[2]) destroyed
all three domains, not just node3 — while the individually-targeted volumes correctly hit only
index 2. Root cause: the nodes are declared with count in infra/tofu/main.tf; -target against a
single count index is a documented Terraform/OpenTofu hazard (the docs explicitly warn -target is
"not for routine use"). count-index addresses are positional and brittle under targeted destroys.
- Recommended fix (Phase-2 IaC hardening): refactor
libvirt_domain.node+ its per-node volumes fromcounttofor_eachkeyed by node name (k3s-dev-1/2/3).for_eachkeys are stable and individually addressable, so a single node can be-replaced/destroyed without touching siblings. - Interim rule: never use
tofu destroy -targeton this module for single-node lifecycle. To cycle one node, usetofu apply -replace='libvirt_domain.node["k3s-dev-N"]'(post-for_each), or destroy the specific VM viavirshand lettofu applyrecreate it. - Silver lining: the over-destroy is exactly what proved IaC validity — disks survived,
tofu applyrecreated the domains, nodes rejoined, Flux reconciled unchanged. The IaC round-trips.
Finding F2 — single-node Ansible rejoin needs the primary in scope for the join-token
Recovering node3 with ansible-playbook ... --limit k3s-dev-3 failed: the k3s_server join task
reads hostvars[groups['k3s_primary'][0]].k3s_node_token, but that fact is only published when the
primary play runs, so a limit that excludes the primary yields
'HostVarsVars' has no attribute 'k3s_node_token'. Worked with --limit 'k3s-dev-1,k3s-dev-3'.
- Recommended fix (Phase-2 IaC hardening): make the join self-contained — have the secondary task
fetch the token on demand via
delegate_to: "{{ groups['k3s_primary'][0] }}"(read/var/lib/rancher/k3s/server/node-tokenat join time) instead of depending on a fact the primary play must have populated in the same run. Then a true single-node rejoin (--limit k3s-dev-N) works hands-off, which is the T-1.5 durability bar.
F1 / F2 — IMPLEMENTED (author-first, 2026-08-16)
- F1 done:
infra/tofu/main.tfnode resources refactoredcount→for_each(keyed by node name); ordered-list/primaryhelper locals added;outputs.tfupdated off the map;moved.tfadds 12movedblocks (os/data/seed/domain × 3).tofu validateOK andtofu plan=0 to add, 0 to change, 0 to destroywith all 12 resources reported "has moved to" — a pure state-address rename, no VM churn. State-persist pending: a follow-uptofu apply(a proven 0-change apply that only rewrites state addresses) is operator-permission-gated; until then the migration re-shows harmlessly on each plan and lands automatically on the next real apply. - F2 done:
roles/k3s_server/tasks/main.yml— secondary now fetches the join token viadelegate_to: primary(with the published fact as a fast path), so--limit k3s-dev-Nrejoin is self-contained.ansible-playbook --syntax-checkOK on all playbooks; role lint clean under the CI gate (make -C ci gatePASS). No cluster change; the running cluster is untouched.
Phase 2 un-suspend PREP — no-secret rollout (author-first, 2026-08-16)
Operator directive: "Phase 2 un-suspend prep … verify cilium parity + fill REPLACE-MEs for the no-secret components … author-first. Does NOT un-suspend/deploy yet." Nothing was deployed; the running cluster is untouched (still 0 Flux Kustomizations / 0 HelmReleases — only controllers + the GitRepository).
Findings from three parallel readiness audits (Longhorn / monitoring / Kyverno)
- Cilium parity VERIFIED (safety-critical): every
[parity]field inplatform/cilium/helmrelease.yamlequals the live CNI —k8sServiceHost=10.73.0.11(rendered by the chart as the pod envKUBERNETES_SERVICE_HOST, not a configmap key — the emptyk8s-service-hostcm value was a red herring), kube-proxy-replacement, gateway-api, egress+bpf masquerade, hubble, l2-announcements, cgroup, v1.16.5. Flux adoption is a no-op reconcile. - Topology: "suspended" = Option A (no root Kustomization was ever created), and the single base
platformKustomization renders all 11 components at once — so a naive un-suspend would drag in the secret-blocked components (cert-manager/externaldns/velero/keycloak/oauth2-proxy/image-automation) and never go healthy (appsdependsOn platform, wait:true). - Longhorn: manifests clean; node prereqs (open-iscsi, /var/lib/longhorn mount, iscsid) are in cloud-init IaC; the REPLACE-ME bulk-NFS SC is isolated/harmless. Ready.
- Monitoring: right-sized (~1.25 GiB) but blocked by (a) needing the
longhornSC first and (b) two by-name secrets (grafana-admin-credentials,alertmanager-smtp) that don't exist — the missing mounts (not the REPLACE-ME text) are what wedge Grafana/Alertmanager. - Kyverno: zero policy violations today (policies scope to
app-namespace=true, none exist), but the fail-closed webhook + 3 HA replicas + Enforce are unsafe/oversized for a cold cluster.
DECISION D-P2A — per-component Flux Kustomizations for the dev no-secret rollout (operator-approved)
Chose Option A (per-component Kustomizations). Authored clusters/dev/rollout/ = 4 Flux
Kustomizations (kyverno → cilium → longhorn → monitoring, monitoring dependsOn: longhorn),
and the dev overlay now $patch: deletes the monolithic base platform/apps and adds ./rollout.
The no-secret tier carries no SOPS content, so the missing sops-age key does not block it
(decryption dropped from these Kustomizations). Secret-blocked components are simply not created
until their hand-offs land.
- cilium uses
prune: false(adopt the out-of-band CNI; never own-delete it). - R-ENV-02 divergence — REVIEW FLAG (architect): the dev overlay now adds resources and diverges
its reconciliation graph from prod (base doc says "dev adds NO resources … graph reused verbatim").
This is a transitional bootstrap scaffold, consistent with the already-inlined dev bootstrap
patches (SOPS/URL). Convergence path is coded in: delete
clusters/dev/rollout/+ the two$patch: deletes to re-adopt the unified base graph once all hand-offs arrive. prod is untouched (still builds the monolith; verified). Flag for architect ratification.
Component hardening (env-gated via cluster-vars so prod keeps the strict posture)
- New base knobs (strict/prod defaults) + dev overrides:
KYVERNO_VALIDATION_ACTION(Enforce→Audit),KYVERNO_WEBHOOK_FAILURE_POLICY(Fail→Ignore),KYVERNO_ADMISSION_REPLICAS(3→1),MONITORING_STORAGE_CLASS(longhorn),GRAFANA_ADMIN_SECRET(grafana-admin-credentials→"" in dev → chart-default password, dev-only/NAT-isolated). - Kyverno webhook also gains an explicit
namespaceSelectorexcluding kube-system/flux-system/kyverno/ longhorn-system/monitoring (belt-and-suspenders vs a fail-closed brick). - Monitoring Alertmanager: the
alertmanager-smtpmount + smtp_*/email_configs are deferred (commented with verbatim re-enable markers) so Alertmanager starts with NULL receivers — alerts fire/route/show in UI+Grafana, just no email until the Phase-4 SMTP hand-off. - Validated:
kubectl kustomizeclean for clusters/dev (exactly the 4 Kustomizations, monolith removed), clusters/prod (monolith intact), and all 4 components; envsubst→YAML-parse of kyverno+monitoring valid;make -C ci gatePASS.
Still required BEFORE an actual deploy (not part of this author-first prep)
- Push current
mainto the host Gitea — Flux source is pinned at6c3f9ed(pre-dates these merges). - Runtime node check:
systemctl is-active iscsid+mountpoint /var/lib/longhornon all 3 nodes. - Cilium adoption mechanism: confirm helm-controller can adopt the Ansible-installed release (was it
helm-installed with a release secret, or CLI/kubectl?) — verify before creating the cilium Kustomization. - Create the dev root Kustomization (the actual un-suspend) — deliberately NOT done here.
KNOWN ISSUES to fix before Phase-2 un-suspend (logged, not yet fixed)
| # | Issue | Impact | Fix |
|---|---|---|---|
| K1 | — | ✅ FIXED: uppercased cluster-vars keys + dev/prod overlay paths; all refs resolve; builds green | |
| K2 | — | ✅ DONE: fixed 3 test bugs (see below), ran via run.sh against the live cluster → R-NET-01 default-deny + R-NET-02 deny-undeclared PASS, JUnit+coverage written, exit 0. chainsaw v0.2.12 installed (host ~/.local/bin) |
|
| K3 | bootstrap/gitea/bootstrap-gitea.sh used a host bind-mount → rootless-podman perms broke app.ini |
initial Gitea start failed | FIXED: switched to a named volume |
| K4 | Kyverno/chainsaw findings during K2 (fixed in the tests): (a) chainsaw does STRICT (length-equal) list matching — asserting one condition against a pod's full conditions[] fails → used scalar phase: Running; (b) a default-deny baseline blocks the kubelet readiness probe (node→pod), so probe-bearing pods never go Ready → dropped probes; (c) chainsaw report path: ../_artifacts resolved to repo-root → exit 1 → fixed to _artifacts; (d) swapped agnhost→busybox httpd (cached, cache-timing-proof) |
— | FIXED in the test files/config |
FINDING for WS-NET (Phase 2, T-2.2 — default-deny baseline design)
The platform's real default-deny baseline must not break kubelet health/liveness/readiness probes
(node→pod ingress). A naive ingress: [] NetworkPolicy in Cilium drops the kubelet TCP probe, so any
pod with a probe never becomes Ready. The baseline must explicitly allow the health-check path (Cilium
host/health-check allowance or an equivalent ingress exception). Surfaced empirically by the R-NET-02
conformance test; carry into T-2.2.
Toolchain note
chainsaw v0.2.12 is installed on the host (~/.local/bin) for local conformance runs; for full
reproducibility it should also be baked into infra/devcontainer (pinned) — the in-cluster Chainsaw
Job (ADR-0011) already uses the pinned image. Deferred (non-blocking).
Blocked on operator (hand-offs)
- T-2.4p Cloudflare API token → cert-manager DNS-01 + ExternalDNS (Phase 2 TLS/DNS).
- T-3.2p PIA username/password → VPN egress gateway (Phase 3).
- OQ-BKP off-cluster backup target → Velero (Phase 4).
- SMTP creds → Alertmanager email (Phase 4).