0db09bf34f
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
79 lines
7.8 KiB
Markdown
79 lines
7.8 KiB
Markdown
# 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.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).
|