From bb548e3c5a17112c90983bd8fa880d7014f49473 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Mon, 29 Jun 2026 22:01:40 +0200 Subject: [PATCH] docs: REPORT for v0.46.0 leaf-regenerate loud-log (live-validated) Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01Pg8ANF97SEeKYSN5Jxw3qJ --- REPORT.md | 101 +++++++++++++++--------------------------------------- 1 file changed, 28 insertions(+), 73 deletions(-) diff --git a/REPORT.md b/REPORT.md index eabe2fd..bdefd2e 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,80 +1,35 @@ -# REPORT — felhom-agent v0.45.0: controller-swap under non-root (stdin `tee` + narrow grants) +# REPORT — leaf lifecycle: signal + loud-log a regenerated leaf, v0.46.0 -**Date:** 2026-06-29 · **Class:** Risky/supervised · **Baseline:** `main` @ `8a4ccab` was v0.44.0 → -**v0.45.0** @ `8a4ccab` (this change). Build sha `a393c8bc…`. **No `felhom-controller` change.** +**Task:** Part B.1 of the F2 + prevention slice — make an accidental local-API leaf **regeneration** +(the 2026-06-28 root→non-root migration class: a state-dir move silently minted a new leaf → every +controller's pin invalidated for days, R1) **visible immediately** instead of silent. -## What & why +**Baseline:** agent `main` @ `1958204` (live 0.45.0) → **v0.46.0** @ `9b0d6c2`, sha `03966da3…`. -Restores fleet controller-swap / managed auto-update under the **non-root** agent — the capability the -2026-06-29 sudoers audit deliberately left broken (its only write vector, `bash -c "printf … >"`, -needed arbitrary in-guest execution). Option A, spike-proven GO -(`felhom.eu/documentation/audits/SPIKE-controllerswap-narrow-grants-2026-06-29.md`). +## Change -## The `writeImage` diff (the only orchestration change) +- **`internal/localapi/cert.go` — `EnsureLeaf` now returns `generated bool`:** false = an existing + pair was LOADED (stable fingerprint); true = a fresh leaf was GENERATED. +- **`cmd/felhom-agent/main.go` — loud call-site:** a load logs `INFO local-api leaf LOADED`; a + regeneration logs **`WARN local-api leaf REGENERATED — any previously issued bootstrap pins are now + INVALID; controllers will fail the pin check until re-bootstrapped (or restore the prior leaf)`** + (with the new fingerprint). The `--selftest=provision` caller updated to the new signature. +- No new sudo/capability surface — pure return + log change. The companion install-script preservation + (`--preserve-state-from` + populated-host guard) is in `felhom.eu/scripts/felhom-host-install.sh`. -``` -- cmd := fmt.Sprintf("printf '%%s\\n' '%s' > %s", image, controllerImageFile) -- _, err := c.exec.GuestExec(ctx, vmid, "bash", "-c", cmd) -+ _, err := c.exec.GuestExecStdin(ctx, vmid, strings.NewReader(image+"\n"), "tee", controllerImageFile) -``` -The image ref is piped on **stdin** into an in-guest `tee` — no shell, no interpolation. `image+"\n"` -is byte-identical to the golden's `printf '%s\n'`; the bootstrap reads `IMAGE=$(cat …)` -(newline-stripping), so the write is consumed identically. `ValidControllerImage` still gates upstream -in `Swap`. Swap orchestration / rollback / state-file logic are **unchanged**. +## Tests (green: `go build/vet/test ./...`) +`TestEnsureLeaf_StableFingerprintAcrossReload`: first call `generated==true`, second `generated==false` +**AND same fingerprint** — persistence keeps the pin stable (a regression that regenerated would flip +gen2 true and change the fp, failing both asserts). -New stdin seam (routes through the SAME `sudo -n` fenced runner — no hand-rolled exec): -`proxmox.Runner.RunStdin` + `ExecRunner.RunStdin` (Run with `cmd.Stdin`), `GuestBinder.GuestExecStdin`, -`GuestExecutor.GuestExecStdin`. The 5 proxmox.Runner test mocks gained a delegating `RunStdin`. +## Live validation (felhom-pve) — both paths PASS +- **Quiet load (the normal case):** deployed 0.46.0; the agent restart logged + `INFO local-api leaf LOADED` fp `60b5974d…` (the leaf existed → loaded, not regenerated). +- **Loud regenerate:** during the F2 born-down test, moving the leaf aside + restart produced + `WARN local-api leaf REGENERATED — any previously issued bootstrap pins are now INVALID …` fp + `751367a2…` — the loud signal fires exactly when a new leaf is minted. Restored cleanly (`leaf + LOADED` `60b5974d…` again). capabilities 45/45 ok throughout. -## Sudoers — `FELHOM_CONTROLLERSWAP` (5 narrow grants) - -``` -/usr/sbin/pct exec [0-9]* -- cat /etc/felhom-controller-image # read (fixed) -/usr/sbin/pct exec [0-9]* -- docker image inspect * # read (target present?) -/usr/sbin/pct exec [0-9]* -- docker inspect -f * # read (running/health/image) -/usr/sbin/pct exec [0-9]* -- systemctl restart felhom-controller-bootstrap.service # restart (fixed unit) -/usr/sbin/pct exec [0-9]* -- tee /etc/felhom-controller-image # write (FIXED path, stdin-fed) -``` -No general `pct exec`, no `bash -c`. Added to the NOPASSWD line. Capability **manifest** gains the 5 -(Critical: a silently-broken fleet auto-update is operator-alert-worthy) — so the v0.44.0 self-probe -watches them and the build-test asserts grant↔code coverage. - -## Tests — all green (`go build/vet/test ./...`, 0 failures) - -- New `TestControllerSwap_WriteViaStdinTee_NoShell`: the write is stdin-`tee` with exact `image\n`, the - `tee` target is the fixed path, and **no** shell vector (`bash`/`-c`/`printf`) appears — would FAIL on - the pre-change impl. -- Existing swap tests (happy / rollback-on-unhealthy / image-absent / no-healthcheck / bad-image / - single-flight) pass over the new write path (fake `GuestExecutor` now models `tee` via stdin). -- `RunStdin` forwards stdin; the 5 mock runners implement it. -- **Build-time coverage:** `TestManifestCoveredBySudoers` covers the 5 new vectors. **Two red-proofs:** - `TestRedProof_DroppedGrantFailsCheck` (lxc-info) + new `TestRedProof_DroppedControllerSwapTeeFailsCheck`. - **Demonstrated red→green on the REAL file:** dropping the `tee` grant → - `capability "controllerswap-write" … NOT covered by any sudoers grant` → FAIL; restored → PASS. - -## Live validation (felhom-pve, guest 9201) — deployed + verified - -- **Deploy:** built `0.45.0` on 180 (sha `a393c8bc…`), shipped 180→local→felhom-pve (sha verified), - backed up prior (`felhom-agent.bak-0.44.0`), `install` + restart → `is-active` = **active**. Sudoers - redeployed via the install mechanism (`visudo -cf` staged → `install -m 0440 -o root -g root` → - re-validate `/etc/sudoers` OK). (Staged file needed a `\r` strip — Windows checkout CRLF — before - visudo; the installed drop-in is LF-clean.) -- **Self-probe (live):** `capabilities self-check ok=45 total=45 degraded=0` — the 5 swap vectors are - granted AND watched. -- **Grant matches (`sudo -n -l`, as felhom-agent):** all 5 → exit 0 - (`cat` / `docker image inspect ` / `docker inspect -f '' felhom-controller` / - `systemctl restart ` / `tee `). -- **Negative controls → denied (exit 1):** `bash -c 'echo pwned'`, `tee /etc/passwd`, - `docker rm -f felhom-controller`, `rm -rf /etc`. The grants are scoped, not a blanket `pct exec`. - -## NOT yet run — live swap (Scenario D) — **awaiting supervised** - -The full end-to-end live swap (controller pre-pulls → swap endpoint → `tee` write → restart → healthy -on target; then a bad image → rollback) **restarts the live controller** and per §13.4 is -operator-supervised. It was NOT run (this session is unattended). When supervised, trigger it via the -controller's real update flow (not a hand-set state) and confirm the tee-write → restart → healthy and -the bad-image rollback over the new write path. - -## Forward note -No code→manifest drift auto-check yet (a future static check of `runner.Run`/`GuestExec*` call sites -vs the manifest). The controller↔agent channel self-health check remains a separate slice. +## NOT changed +The pin/transport, the token store, the sudoers/capability surface, the drive gate. Cert lifecycle +signalling + logging only.