Files
felhom-agent/REPORT.md
T

4.3 KiB

REPORT — felhom-agent v0.23.0: device-ROLE classification + tiered storage-wipe gate

What this implements

The agent half of the storage-authorization redesign (CC SPEC, Part A). Destructive storage wipes are now tiered by the device's protection ROLE, classified by the agent from its own inspection — never the controller's or hub's claim.

A1 — Device-ROLE classification (authoritative)

  • New internal/storage/role.go: DeviceRole = system | backup | user-data.
  • SystemDisks(host) resolves the OS whole-disks (the disks backing /, /boot, /boot/efi) from the mount table + world-readable /dev symlinks (root-free; the root-CLI fence is untouched).
  • RoleForStorage(typ, backingDevice, …) tiers a storage-view target; RoleForRawDevice(device, …) tiers a raw device (a fresh disk in the init flow, not yet a PVE storage).
    • pbsbackup; lvmthin / builtin local / nfs / cifs / unknown → system; usb / local-dir on a non-system external deviceuser-data.
    • Fail-safe: any ambiguity → system (most-protected) — consistent with destructive-on-ambiguity.
  • GET /disks → each DiskInfo now carries role.

A2 — Gate tier (internal/reconcile)

  • New CustomerConfirmable disposition + StorageRole{System,Backup,UserData} (string mirror of storage.DeviceRole, kept in reconcile to avoid a storage→reconcile import edge).
  • Gate.AuthorizeStorageWipe(StorageWipeAuthz, signed):
    • user-data → allowed iff Confirmed AND the confirmation's durable id matches the device's agent-re-resolved durable id (ReasonCustomerConfirmed); unconfirmed → ReasonPendingConfirmation; mismatch / missing durable → ReasonBindingMismatch. Audited with the durable id.
    • system/backup/unknown → the standard operator-signature path (pending_signature); Confirmed is ignored — refused by role.
  • AuditRecord.DurableID added (the customer-visible "who/what/when + durable id").

A3 — Endpoint plumbing (internal/localapi)

  • POST /disks/format accepts confirmed + durable_id. The data-bearing path:
    • resolves the device's authoritative role (deviceRole — prefers a matching storage target's role so PBS is recognized as backup, else raw classification), re-resolves the durable id, and calls the tiered gate.
    • user-data confirmed → mkfs (200); user-data unconfirmed/mismatch → 403 with needs_confirmation
      • the durable id (no opsign); system/backup → 403 with the operator-signature pending op.
    • blank devices stay benign mkfs (unchanged).
  • StorageGate interface widened: AuthorizeWipe(WipeRequest) WipeDecision. Adapter in main.go bridges to Gate.AuthorizeStorageWipe.
  • Eject stays benign (unmount, data preserved) — its customer confirmation is a controller-side UI concern, not a gate tier (per the hard rule: confirmation walks the gate ONLY for user-data storage_wipe).

Tests (non-hollow)

  • internal/storage/role_test.go — demo-storage mapping (local/local-lvm/felhom-pbs protected, felhom-usb on /dev/sdb1 user-data) + fail-safe when system disks are unknown.
  • internal/reconcile/storage_wipe_test.go — a confirmed:true wipe on system/backup is refused (falls to pending_signature, no exec); user-data durable-id mismatch refused; missing durable refused; unconfirmed → pending_confirmation; unknown role fails safe.
  • internal/localapi/disks_test.go — format-handler branches: user-data confirmed → mkfs; user-data unconfirmed → 403 needs_confirmation with no operator-signature pending op; confirmed-but-protected → still refused (role beats confirmation); blank → mkfs without the gate.

go build ./... && go vet ./... && go test ./... all green (Go 1.26, local).

Version

v0.22.0 → v0.23.0 (version var in cmd/felhom-agent/main.go).

Notes / residuals

  • The one accepted residual (per spec): a compromised controller could mkfs a user-data drive it could already rm -rf — same data loss, no new reach. System/backup remain operator-only.
  • LVM-root caveat: when / is on device-mapper, the OS whole-disk is pinned via /boot//boot/efi (a raw ESP partition on the OS disk on standard Proxmox/Debian installs); the fail-safe default covers anything that cannot be resolved.