Files
felhom-agent/REPORT.md
T

79 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# REPORT — blast-radius audit fixes B1 + D1 + D2 + D3 (v0.61.0)
**Date:** 2026-07-03 · **Repo:** `felhom-agent` · **Class:** Implementation (destructive-op guard
surfaces D1/D3 proven in unit-land + a loop-device recorder harness — NO real mkfs, no live /dev race,
per the §13 STOP). Source of truth:
`felhom.eu/documentation/audits/AUDIT-blast-radius-hostroot-localapi-2026-07-02.md`.
## 1. Baseline → target
agent **v0.60.0** (`cc93dae`, confirmed before starting) → **v0.61.0**. Commits on `main`:
- **`3f382bf`** — all four fixes + tests + harness + version bump + CHANGELOG/CONTEXT/README.
- (this REPORT is the follow-up docs commit)
## 2. What shipped
| Fix | Files | What changed |
|---|---|---|
| **B1** random temp staging | `internal/guesthook/install.go`, `internal/localapi/intermediary.go` (new `stageTemp`), `configs/felhom-agent.sudoers`, `internal/capability/manifest.go` | Fixed, predictable `/tmp` names (local TOCTOU into root-executed hookscript/boot script) → `os.CreateTemp` random names (the `lanresolver` pattern). Sudoers install-SOURCE grants became globs (`/tmp/felhom-guest-hook-*.sh`, `/tmp/felhom-shared-parent-*.{sh,service}`); destinations + modes unchanged. Manifest repr vectors updated to match. |
| **D1** wrapper asymmetry | `configs/felhom-mkfs-guarded.sh`, new `scripts/mkfs-guarded-harness.sh` | Additive re-checks: `/sys/block/<disk>/ro`==1 → die; lsblk-FSTYPE loop refusing exactly `claim.go`'s `memberFSTypes` (LVM2_member/zfs_member/linux_raid_member/crypto_LUKS/swap); pvs via absolute candidates (`/usr/sbin/pvs`, `/sbin/pvs`) with the FSTYPE loop as the independent LVM catch when pvs is absent. Nothing removed/reordered; a blank partitioned disk still formats. |
| **D2** empty-lsblk fail-safe | `internal/storage/claim.go` | `classifyClaim` refuses when the node tree is EMPTY or the target whole-disk is absent from it (previously: empty-but-successful lsblk ⇒ `unclaimed`). Placed after the member/mount loop; the `gatherErr`/`wholeDiskOK`/`isSystem`/`readonly`/`lvmPV`/`zfsMember` ordering untouched. |
| **D3** blank-format anti-retarget | `internal/localapi/wipe_reresolve.go` (shared `antiRetargetResolveExpect` core + new `antiRetargetResolveBlank`), `disks.go`, `formatjob.go`, `server.go` (new `reresolveBlank` seam) | The blank branch now derives the device's durable id (**no durable id ⇒ 409 refuse** — path-only formats not permitted), re-resolves it (resolve → re-derive exact match → re-inspect **STILL !DataBearing**) and formats the RE-RESOLVED device, never `req.Device`. The format-job record carries `blank`; restart recovery re-checks blank jobs with the blank variant. Confirmed (data-bearing) branch untouched. |
**Not in this batch (per spec §0/§12):** A1 (stale-lock pool-membership — a SPIKE: the role lacks
`Pool.Audit`, no cheap membership read exists); C1, C2, A2, B2B5, E1, E2 (deferred). B2's `..` guard
intentionally skipped — `DriveNameFromRaw` already rejects `/`, so `..` alone can't traverse.
## 3. Tests + red-proofs (each new test shown FAILING on the pre-fix impl, then green on the fix)
| Fix | Tests | Red-proof result |
|---|---|---|
| B1 | `TestInstallSnippet_RandomTempName`, `TestInstallSharedParent_RandomTempName` (fake runner records the install source; random pattern + two-calls-differ + content + cleanup asserted) | Pre-fix (HEAD files restored): both FAIL — fixed name, identical across calls. Fix restored → green. |
| D2 | `TestClassifyClaim_EmptyNodesRefused`, `TestClassifyClaim_TargetAbsentFromTree` | Pre-fix `claim.go`: both FAIL — `(true,"unclaimed")` returned. Fix restored → green. |
| D3 | Pure: `TestFormatBlankPath_AntiRetarget_{ReassignedDataBearingRefused,ReassignedDifferentDiskRefused,UnresolvableRefused,SameBlankProceeds}`. Handler: `TestFormat_Blank_{FormatsReresolvedDeviceNotCallerPath,ReresolveRefusalNoMkfs,NoDurableIDRefused}` | Pre-fix blank branch (formats `req.Device`, no binding): all 3 handler tests FAIL — mkfs ran on the caller path with HTTP 200 and no durable binding. Fix restored → green. |
| D1 | `scripts/mkfs-guarded-harness.sh` on felhom-pve (root): throwaway loop devices (`losetup`, one `-r` read-only), PATH-shimmed `lsblk` FSTYPE fixture, `/dev/null` bound over pvs for the pvs-absent case, and a RECORDER bind-mounted over `/usr/sbin/mkfs.ext4` in a private mount ns — **no real mkfs possible**; "formats" is proven by the recorder argv, refusal by its absence | **Fixed wrapper: 8/8 PASS** (5 member FSTYPEs refused, read-only refused, pvs-hidden LVM still refused, plain blank disk still reaches mkfs). **Pre-fix wrapper: 7/8 FAIL** — every member/RO/pvs-hidden fixture reached mkfs. The loop-device `-b` check ran for real (no stub). |
Full gates green on Windows AND on the build server (go1.26.0):
`go build ./... && go vet ./... && go test ./...`**0 failures**. Test count **472 → 483** (+11).
## 4. Build / publish / deploy / verify
- Built on 180 from `3f382bf`: `CGO_ENABLED=0 go build -ldflags "-X main.version=0.61.0"`;
`--version``felhom-agent 0.61.0`.
- **Published to Gitea** (generic package, delete-then-put + round-trip GET verified):
`AGENT_VERSION=0.61.0`, `AGENT_SHA256=d5f2f283501dac9daac1c197c9665c98f4577e11d155f5da831b26048c736ed8`.
Credentials sourced out-of-band (`/root/.golden-secrets.env` on the host; not committed anywhere).
- **Deployed to felhom-pve** (backups kept: `felhom-agent.bak-0.60.0`, `felhom-mkfs-guarded.bak-0.60.0`):
binary → `/usr/local/bin/felhom-agent`; **new wrapper**`/usr/local/sbin/felhom-mkfs-guarded`
(grep-confirmed the D1 checks in the installed copy); **new sudoers**`/etc/sudoers.d/felhom-agent`
(`visudo -cf` passed both staged and installed).
- **Verify after restart:** service `active`, `--version` 0.61.0, **0 ERROR lines** in the startup
journal; **capability self-check 46/46 ok, 0 degraded** (proves the new sudoers globs cover the new
manifest vectors live); `ReassertGuestBinds`/reconcile re-bound exactly the three enrolled drives
(felhom-flash, felhom-usb, teszt_enroll) — nothing else; stale-lock recovery ran with nothing to reap
(no stale locks, no false action); local API listening; hub desired-state pulled.
- **B1 live-proven end-to-end:** made the installed shared-parent script stale (appended a marker) →
the next reconcile self-healed it through the REAL sudo path with random-named temps
(`install -m 0755 -- /tmp/felhom-shared-parent-281383295.sh …`,
`install -m 0644 -- /tmp/felhom-shared-parent-2841067761.service …`) — authorized by the new globs.
- Cleanup: all /tmp staging removed from felhom-pve and 180; harness loop devices/workdir self-cleaned.
## 5. NOT yet live-validated — awaiting a supervised step
- **D1 destructive proof:** a real `mkfs` attempt against a crafted ZFS/mdraid/LUKS/swap member disk.
- **D3 race proof:** a live /dev re-enumeration race during a real format.
Both are proven in unit-land + the recorder harness only, per the task's hard STOP.
## 6. Observations / follow-ups (recorded, not acted on)
- **Operator follow-up:** the hub Day-0 artifact manifest still vouches agent **0.60.0** — record
`0.61.0` / sha `d5f2f283…` in the password-gated operator UI (Configs → Day-0 artifacts) so Day-0
self-installs fetch the new build. (CC cannot do this; see hub-artifact-manifest gotcha.)
- **A1 (LOW, pending SPIKE):** stale-lock recovery reaps by a pool-blind `ListLXC` scan — contained by
the pool-scoped token; MUST be fixed before any broad/root-token deployment. Open design question:
which pool-membership read the role can authorize (`Pool.Audit` absent today).
- Deferred audit items: C1 (controller-swap version floor), C2 (NAS server allowlist), A2 (gate
journal cross-check), B3/B4/B5 housekeeping, E1/E2 comment fixes.
- Known pre-existing: the non-root agent cannot read the PBS key (`felhom-pbs.pw` permission denied at
startup — the BUNDLE leftover, unchanged by this batch).