d1bd44d2d5
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
50 lines
3.4 KiB
Markdown
50 lines
3.4 KiB
Markdown
# REPORT — felhom-agent v0.24.0: role-gate the eject path
|
|
|
|
## What changed (Part A of the storage-fixes spec)
|
|
|
|
`POST /disks/eject` is now **role-gated at the agent**. Previously `handleDiskEject` validated scope +
|
|
`where`, computed dependents, then unconditionally called `Unmount` — so a direct API call (or a
|
|
compromised controller) could `POST /disks/eject {where:"/var/lib/vz"}` (or the PBS mount) and unmount
|
|
a protected storage. The controller UI hid the button for system/backup, but the **agent did not
|
|
enforce it**, contradicting the "system is never unmountable" invariant and the defense-in-depth the
|
|
wipe path upholds.
|
|
|
|
### A1 — refuse eject of system/backup (`internal/localapi/disks.go`)
|
|
- `handleDiskEject` now, before `Unmount`, resolves the **authoritative protection role** of the
|
|
storage mounted at `where` via the new `roleForMountPath` (the agent's own storage-view +
|
|
host-topology classification — `RoleForStorage` keyed on `MountPath == where` — never the caller's
|
|
claim). It refuses **403 with no `Unmount`** unless the role is `user-data`.
|
|
- **Fail-safe**: a view error or no storage target found at `where` → treated as `system` (most-
|
|
protected) → refused. Consistent with the wipe gate's most-protected-on-ambiguity default.
|
|
- Mirrors the wipe path's logging: `"protected — eject refused by role"`.
|
|
- `hostReader()` seam added (`Options.HostReader`, optional; defaults to the production
|
|
`*storage.ProcHostReader`) so the role classification (`SystemDisks`) is unit-testable. `handleDisks`
|
|
and `deviceRole` now share the same seam (no production behaviour change — default is the real reader).
|
|
|
|
### A2 — non-hollow test
|
|
- `TestEject_RoleGated` asserts a `system` mount and a `backup` (PBS) mount are **refused with no
|
|
`Unmount`**, a `user-data` mount **ejects** (Unmount called once), and an unresolvable mount **fails
|
|
safe to refused** — the same non-hollowness (executor-not-invoked) the wipe tests use.
|
|
- `TestEject_UnmountAndDependents` updated to a user-data target so the role-gate permits it.
|
|
|
|
## Build / deploy
|
|
- Version bumped `0.23.0 → 0.24.0` (`cmd/felhom-agent/main.go`). CHANGELOG updated (newest on top).
|
|
- Built on the build server (go1.26) with `-ldflags -X main.version=0.24.0`, installed to
|
|
`/usr/local/bin/felhom-agent` on `felhom-pve` (prior binary kept as `.bak-0.23.0`), service
|
|
restarted — `felhom-agent --version` → `0.24.0`, local-api listening on `192.168.0.162:8443`.
|
|
|
|
## Live validation (felhom-pve, guest 9201)
|
|
- `GET /disks` (guest 9201): roles `felhom-usb=user-data`, `local=system`, `felhom-pbs=backup`,
|
|
`local-lvm=system`.
|
|
- Hand-issued `POST /disks/eject {where:"/var/lib/vz"}` (the `local` system storage's real mount) →
|
|
**HTTP 403**, body `mount is system/backup-protected — eject refused (role: system)`; agent logged
|
|
`protected — eject refused by role … role=system`; **no unmount** occurred.
|
|
- `POST /disks/eject {where:"/mnt/nonexistent"}` (no storage target) → **HTTP 403** (fail-safe).
|
|
- `/mnt/felhom-usb` (the live user-data data drive) was deliberately **not** live-ejected to avoid
|
|
disrupting the running guest; the positive user-data eject path is covered by the non-hollow
|
|
`TestEject_RoleGated` (asserts `Unmount` is called exactly once for a user-data mount).
|
|
- `go test ./...` green for the whole module.
|
|
|
|
## Notes / not done
|
|
- No golden change required for Part A — the agent runs host-side, not baked into guests.
|