Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
7.8 KiB
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, B2–B5, 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.envon 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 -cfpassed both staged and installed). - Verify after restart: service
active,--version0.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
mkfsattempt 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/ shad5f2f283…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
ListLXCscan — 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.Auditabsent 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.pwpermission denied at startup — the BUNDLE leftover, unchanged by this batch).