diff --git a/REPORT.md b/REPORT.md index 9c63971..4e11d00 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,82 +1,82 @@ -# REPORT — restore guests INTO the felhom pool (pool-scoped-ACL enabler, agent v0.53.0) +# REPORT — Impl-1: format-safety foundation (agent v0.54.0) -**Date:** 2026-07-01 · **Repo:** `felhom-agent` · **Class:** Risky/supervised (agent code normal; the -live token re-scope was the supervised step — executed + validated on felhom-pve). +**Date:** 2026-07-01 · **Repo:** `felhom-agent` · **Class:** Risky/supervised (destructive `Format`/mkfs +path + sudoers). Built/tested/deployed normally; live validation proves the guard REFUSES dangerous +targets (safe) and formats only the throwaway `/dev/sdd`. -Colleague-safety batch #4 phase b (agent half). Makes every restore allocate the guest INTO the `felhom` -pool so the agent token can be scoped to `/pool/felhom` + `/storage/` (blast-radius containment -on a shared host). Grounded by `felhom.eu/documentation/audits/SPIKE-pool-scoped-acl-2026-07-01.md` (PASS). +Impl-1 of the drive-enrollment series (SPIKE-drive-enrollment-2026-07-01), built BEFORE the enrollment +feature to harden the existing format path. ## 1. Baseline → target -felhom-agent `0573ec9` (v0.52.0) → **v0.53.0**. No engine/ACL logic beyond the restore param. +felhom-agent `7639ab5` (v0.53.0) → **v0.54.0**. -## 2. Files changed + commit +## 2. Root cause + files changed -- `internal/proxmox/mutate.go` — `RestoreLXCOptions.Pool` + `RestoreLXC` sends `pool=` when non-empty. -- `internal/reconcile/bringup.go` — `const DefaultPool = "felhom"`; `BringUpSpec.Pool`; bring-up restore - passes `Pool: spec.Pool`. -- `internal/reconcile/restoretest.go` — scratch restore passes `Pool: DefaultPool` (residual #2). -- `cmd/felhom-agent/main.go` — both `BringUpSpec` literals (bring-up/DR + provision) set `DefaultPool`. -- Tests: `internal/proxmox/restore_test.go` (`TestRestoreLXC_PoolParam`), `internal/reconcile/bringup_test.go` - (`TestRestoreSitesUsePool`). `CHANGELOG.md`, `README.md`. -- Commit **`7639ab5`** on `main` (parent `33e3443`). +`Format` delegated authorization to its caller and only checked `DataBearing` (has-data) — insufficient: +the OS disk is data-bearing yet catastrophic; and the sudoers permitted `mkfs.* /dev/*`. + +- `internal/storage/claim.go` (new) — the **unclaimed-disk filter**: `classifyClaim` (pure) + + `gatherClaimFacts` (SystemDisks + lsblk member-FSTYPE + foreign-mount + RO + authoritative pvs/zpool; + fail-safe → CLAIMED). +- `internal/storage/hostops.go` — mandatory guard at the top of `Format` (refuse unless unclaimed); + `Binaries.MkfsGuarded`; `Format` now execs the guarded wrapper; `SudoHostOps` gains a `HostReader`. +- `configs/felhom-mkfs-guarded.sh` (new) — the root wrapper (below-the-agent gate). +- `configs/felhom-agent.sudoers` — `FELHOM_FORMAT` drops raw `mkfs.*`, allowlists only the wrapper; + read-only `pvs`/`zpool status -P` added to `FELHOM_DISK`. +- `internal/storage/claim_test.go` (new), `hostops_disk_test.go` (updated), `internal/capability/manifest.go` + (mkfs sample → wrapper). `CHANGELOG.md`, `README.md`, version. +- Commit **`5209830`** on `main` (parent `05f22a9`). ## 3. Green gate `go build ./...`, `go vet ./...`, `go test ./...` — clean (local + build server 180). -## 4. Tests + red-proofs (§10) +## 4. Tests + red-proof -- `TestRestoreLXC_PoolParam` PASS: `Pool:"felhom"` → `pool=felhom` in the POST body; `Pool:""` → key - absent. **Red-proof:** unconditional `v.Set("pool",…)` → the empty case sent `pool=` → omit assertion - FAILED as designed; reverted. -- `TestRestoreSitesUsePool` PASS: the provision bring-up threads `spec.Pool` and the restore-test - hardcodes `DefaultPool` → both captured `RestoreLXCOptions.Pool == "felhom"`. **Red-proof:** removed - `Pool` from both sites → the provision assertion FAILED; reverted. +- `TestClassifyClaim` (pure, table-driven): every claim signal (system/LVM-PV/ZFS/mdraid/member-FSTYPE/ + foreign-mount/RO/undeterminable) → CLAIMED; fail-safe (gather error) → CLAIMED; clean disk + our-own- + drive-re-init → unclaimed. **Red-proof:** the "system/OS disk (non-data-bearing)" case — removing the + `isSystem` check flips it to `unclaimed=true` → the test FAILS (proves the guard adds safety beyond + `DataBearing`, which would call a blank OS disk benign). Verified fail, reverted. +- `TestFormatGuard_{RefusesSystemDisk,RefusesLVMMember,AllowsUnclaimed}`: the guard is wired into + `Format` — refuses claimed devices (no mkfs), allows unclaimed (invokes the wrapper). +- `TestParseLsblkNodes`; capability manifest coverage test updated + green. -## 5. Published + hub manifest +## 5. Deploy + live validation (felhom-pve — SAFE, only /dev/sdd formatted) -- Gitea generic package **`admin/generic/felhom-agent/0.53.0/felhom-agent`**, sha256 - `9d1ef8298dcc5090936afa1b88f1612c0b5f03a62548ebbe0989d34d6622aa61` (publish did delete-then-PUT + - round-trip GET verify). -- **Hub artifact manifest — REQUIRED operator follow-up (mandatory for fresh installs):** the served - manifest still advertises agent **0.52.0**. A FRESH install now runs script v1.6.0 (scoped ACL + - `Pool.Allocate`), which is only correct with agent ≥ v0.53.0 (restore `--pool`); with 0.52.0 the guest - restores OUTSIDE the pool and the scoped token then 403s on it. **An operator must set the artifact - manifest to agent 0.53.0 / sha `9d1ef8298dcc5090936afa1b88f1612c0b5f03a62548ebbe0989d34d6622aa61`** in - the hub UI (now a dropdown, auto-sha — hub v0.29.0) before any fresh v1.6.0 install. The demo swap did - NOT depend on this (binary deployed directly). +Published Gitea `felhom-agent/0.54.0` (sha `0500dd9bd15b03e67e8c93c0ac7aa8008d7413c335e6f852aeaf074d3b364eda`). +Deployed to felhom-pve in order: install wrapper (0755) → swap sudoers (visudo-validated) → agent binary +(backup `.bak-0.53.0`) → restart. (Note: my manual scp of the Windows working-copy sudoers had CRLF and +failed `visudo`; fixed live by CR-stripping + re-install. The committed repo file is LF and the +host-install `fetch_raw`-from-Gitea path is unaffected.) -## 6. Supervised swap on felhom-pve — executed + validated (all gates PASS) +| Proof | Result | +|-------|--------| +| **P-1** wrapper on `/dev/sda` (OS) via agent sudo path | ✅ REFUSED "system/OS disk (/dev/sda backs /boot/efi)", exit 1, no mkfs | +| **P-2** wrapper on `/dev/sda3` (the LVM PV) | ✅ REFUSED (system), no mkfs | +| **P-3** raw `sudo mkfs.ext4 -F /dev/sda` as felhom-agent | ✅ DENIED ("a password is required" — not allowlisted; Part B) | +| **P-4** wrapper on `/dev/sdd` (unclaimed throwaway) | ✅ ACCEPTED → mkfs.ext4 → `blkid /dev/sdd` = `TYPE="ext4"` | +| agent-guard live reads (as felhom-agent) | ✅ `sudo pvs` / `sudo lsblk ` / `sudo zpool status -P` all OK → the guard can gather on the live host | +| post-deploy health | ✅ agent 0.54.0 active; `--selftest=read` = 5 storages (3b-fix intact); 9201 running | -Sequence: `systemctl stop felhom-agent` → `felhom-host-install.sh --rescope-acl --vmid 9201` (added the -3 scoped roles+grants incl. `/storage/felhom-pbs`; removed the old broad `FelhomAgent` `/` grant + role) -→ deploy `felhom-agent 0.53.0` (backup `.bak-0.52.0`) → `systemctl start`. Validation, all under the -scoped token: +**Part A (agent guard)** is unit-tested (logic, incl. red-proof) and its live host reads are confirmed +working as the agent; **Part B (wrapper)** is live-proven on real devices (sda/sda3 refused, sdd +accepted) + raw-mkfs denied. -| Gate | Result | -|------|--------| -| A selftest=read | ✅ OK — now sees only the pool guest (9201) + the 3 scoped storages (was 5+all guests) | -| B provision-into-pool | ✅ `--selftest=bring-up -mode provision -vmid 9300` → boot+running, 9300 landed in pool (restore-into-pool + SetConfig + start all scoped) | -| C **PBS under scope (residual #1)** | ✅ vzdump 9300 → `felhom-pbs` (200, task OK) + restore-from-PBS → 9301 pool=felhom (200, OK, landed in pool); cleanup 200 | -| D **restore-test into pool (residual #2)** | ✅ scratch 990000 restored into pool, boot+running, torn down, `pass=true` | -| E blast-radius | ✅ non-pool guests 9001/9100 → 403 (`VM.Audit`/`VM.PowerMgmt`); ungranted `felhom-usb` → 403 | -| F 9201 health | ✅ running; controller container Up (healthy) — agent↔guest channel intact; `--selftest=hub` OK; journal clean; pool = `[9201]` | +## 6. NOT exercised / deferred -Cleanup: scratch 9300/9301/990000 destroyed, PBS + local scratch snapshots freed, `.bak-0.52.0` rollback -binary retained. The live `felhom` pool holds only 9201. +- The `/disks/format` local-API endpoint was not invoked live (needs a guest local-API token). The + guard it calls is identical to the unit-tested code, and its live reads + the wrapper below it are + proven on real devices — so the stack is validated, just not through the HTTP entrypoint. +- **Impl-3 residual:** a raw disk passed through to ANOTHER VM looks unused to the host — a host-level + filter can't detect it; the shared-box operator gate (Impl-3) closes that. Impl-1 closes everything + host-visible (a strict improvement over the prior no-guard state). -## 7. NOT yet live-validated — awaiting supervised run +## 7. Observations -- **The colleague's-box migration** (single-node, so no cluster nuance) — a later supervised repeat of - the §6 stop→rescope→deploy→start + gates, once the hub manifest serves 0.53.0. - -## 8. Observations - -- Storage blast-radius is a bonus: the scoped token also 403s on ungranted storages (felhom-usb) — read - containment beyond guests. -- The agent's `ListLXC`/`ListStorage` now return filtered lists (pool guests / scoped storages) — the - host-report naturally scopes to Felhom's own footprint. Correct + intended. -- felhom-pbs is the offsite backup target; the `Store` grant on `/storage/felhom-pbs` is mandatory (was - the SPIKE's main unvalidated residual — now proven live). +- The guard's mount rule ALLOWS Felhom's own drives (mounted under `/mnt/felhom-drives`) for re-init; + their DATA is protected by the separate DataBearing wipe-confirm, not this guard. So the spec's "refuse + sdb/sdc" was not applicable to the guard (they're re-initable Felhom drives) — I did not mkfs them + (customer data), per the STOP. +- The throwaway `/dev/sdd` is now ext4 (was ntfs); unmounted, not enrolled. Safe to unplug.