Files
felhom-agent/REPORT.md
T

6.3 KiB

REPORT — v0.62.0: A1 pool-membership ownership check for the stale-lock reaper

Date: 2026-07-03 · Class: implementation (agent v0.61.0 → v0.62.0, live on felhom-pve) Spec basis: audit A1 (AUDIT-blast-radius-hostroot-localapi-2026-07-02 §A) via the spike verdict (felhom.eu/documentation/audits/SPIKE-a1-pool-membership-read-2026-07-03.md Q3/Q4/Q5). Companion: felhom.eu felhom-host-install.sh v1.8.0 → v1.9.0 (Pool.Audit in FelhomAgentGuest).

1. Baselines & commits

Repo Baseline Shipped commit
felhom-agent 3e760a559a (v0.61.0) 3f37c5f — v0.62.0 code + tests + REUSE/CHANGELOG/CONTEXT
felhom.eu 5e3dfcb714 (script v1.8.0) 639a57e — script v1.9.0 + scripts/CHANGELOG + REUSE §2 errata + CONTEXT

2. What changed (agent)

  • Client.Pool(ctx, name) (proxmox/query.go): GET /pools/{name}PoolInfo{PoolID, Members[]{VMID,Type}}. API-side read — the Client-never-shells / Privileged-never-HTTPs fence is untouched.
  • staleLockController.Guests() (localapi/stalelock.go): ListLXC ∩ pool members (nonzero-vmid, non-storage entries). Pool-read failure → wrapped error pool membership read (pool=felhom): … → rides the pre-existing "guest list unavailable — skipping recovery" guard (stalelock.go Server path). NEVER falls back to the unfiltered list. One INFO line per scan: stale-lock: scanning pool guests (pool, listed, scanned). Note (spec deviation, by necessity): the spec located this line in RecoverStaleLockedGuests; it is emitted by the production controller instead, because the unchanged StaleLockController seam cannot carry the pool name + pre-intersect count to the Server without breaking the 9 existing fake-based tests. Net startup-log effect is identical (Scenario D line observed live, §5).
  • NewStaleLockController(px, runner, pool, logger); main.go threads reconcile.DefaultPool.
  • Capability: composed closure in main.go appends pve:pool-read (non-critical) to the sudo prober's snapshot; internal/capability/ untouched. --selftest gained a "pool read" line.

3. Test results (all green: go build ./... && go vet ./... && go test ./..., 17 pkgs ok)

Test Result
TestStaleLock_ForeignGuestNotReaped (Scenario A) PASS — foreign 5000 never unlocked/snap-deleted/started
TestStaleLock_PoolGuestStillReaped (Scenario B) PASS — owned 9201 fully recovered (unlock+delsnap+start)
TestStaleLock_PoolReadFails_SkipsAll (Scenario C) PASS — zero mutations on pool-read error; error text names the pool read
TestStaleLockController_GuestsIntersect (§8 edges) PASS — storage-type/vmid-0 members excluded; empty pool = empty scan, no error
9 pre-existing stalelock tests PASS unmodified (Server-level fakeStaleLock seam untouched by design)

Red-proofs (both run and reverted):

  • A: intersect replaced with raw-ListLXC passthrough → TestStaleLock_ForeignGuestNotReaped FAILED with runner calls=[[pct unlock 5000]] (the exact A1 exploit) and TestStaleLockController_GuestsIntersect failed with both vmids returned. Restored.
  • C: pool-read error changed to fall back to the unfiltered list → TestStaleLock_PoolReadFails_SkipsAll FAILED with runner=[[pct unlock 9201]] delsnap=[9201] started=[9201]. Restored.

4. Live rescope (felhom-pve, PUSHED v1.9.0 script fetched from Gitea raw, --rescope-acl --vmid 9201)

Roles after (only Guest changed; Base/Store byte-identical):

FelhomAgentStore → Datastore.Allocate,Datastore.AllocateSpace
FelhomAgentGuest → Pool.Allocate,Pool.Audit,VM.Allocate,VM.Audit,VM.Backup,VM.Config.CPU,
                   VM.Config.Disk,VM.Config.Memory,VM.Config.Network,VM.Config.Options,
                   VM.PowerMgmt,VM.Snapshot,VM.Snapshot.Rollback
FelhomAgentBase  → Datastore.Audit,SDN.Use,Sys.Audit

Production-token GET /pools/felhom (403 in the spike) → HTTP 200, members [9201]. The running v0.61.0 agent stayed active through the rescope (order: rescope BEFORE agent deploy, per spec).

5. Agent deploy verification (felhom-pve)

  • Binary built on 180 from 3f37c5f, deployed with backup felhom-agent.bak-0.61.0; felhom-agent --versionfelhom-agent 0.62.0; service active.
  • Startup log (live excerpt): msg="capabilities self-check" ok=47 total=47 degraded=0 (46 → 47: the new pve:pool-read, OK) and msg="stale-lock: scanning pool guests" pool=felhom listed=1 scanned=1 (listed=1 because the scoped token's ListLXC is already pool-filtered — spike T1, expected). ReassertGuestBinds clean (logs only on action/failure; none emitted). No new warnings — the pre-existing PBS token-secret WARN (BUNDLE leftover, CONTEXT open thread) is unchanged.
  • --selftest: [ ok ] pool read pool "felhom", 1 member(s) / - 9201 type=lxc; suite OK.
  • Hub-side: latest stored host report (agent_version 0.62.0) carries {"name":"pve:pool-read","feature":"stale-lock recovery scoping (pool ownership check)", "critical":false,"status":"ok"} — 47 capabilities, 0 degraded. Verified against a snapshot of the hub DB (host_reports.report_json), since the operator UI is password-gated.
  • Published: Gitea generic package felhom-agent/0.62.0, round-trip sha256 verified: ed1192c9ae8f355dee19128cf4b01984ba7398e860b4a16a8009655b84b32d81.

6. Operator follow-ups

  • Hub Day-0 artifact manifest still vouches 0.60.0 → bump to 0.62.0 (sha256 above; password-gated UI — CC cannot).
  • Rotation + de-git of the 3 committed secrets in felhom.eu/manifests/felhom.secret.yaml (spike appendix; REUSE §2 errata now records that gitea-creds is NOT out-of-band).

7. Observations (not acted on)

  • Hub capability-name handling confirmed generic: the checker keys on Critical && status=="degraded" only (hub/internal/monitor/host_capability.go), names are data — the new non-critical entry rides the report/UI without paging and needed no hub change.
  • The spike Q6 consumer observations (hub collect / disks / reconcile ListLXC users) remain observations; no other consumer touched, per spec.
  • _state_get provisioned_vmid was empty on felhom-pve (state.json holds only completed), so --rescope-acl needed the explicit --vmid 9201 — worth knowing for other boxes.