e51b3a2f66
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
80 lines
5.6 KiB
Markdown
80 lines
5.6 KiB
Markdown
# REPORT — slice 8A (agent half): local-API server + provisioning back-half (v0.10.0) (2026-06-10)
|
|
|
|
> Overwrite-latest report (most recent significant work only). Cumulative history lives in
|
|
> [CHANGELOG.md](CHANGELOG.md). Implements the host-agent half of `TASK — Slice 8A`. The in-guest
|
|
> controller half is in `felhom-controller` v0.35.0. No hub change. Grounded by
|
|
> `felhom.eu/documentation/tests/slice8a-channel-deploy-spike-findings.md` (commit `4a81a96`).
|
|
|
|
## Outcome
|
|
|
|
The agent now exposes the **per-guest local API** (doc 03 §6) the in-guest controller calls over the
|
|
bridge, and runs the **provisioning back-half** that follows the slice-7 bring-up front half. With
|
|
the slice-7 front half, this completes the **full provisioning chain**: provision a guest → deploy
|
|
its (golden-baked) controller → controller comes up configured and talks to the agent — **validated
|
|
live end-to-end on the demo**.
|
|
|
|
## What landed
|
|
|
|
- **`internal/localapi`** — the HTTPS local-API server, the per-guest authorization gate.
|
|
- Serves a **persisted self-signed leaf** with a **stable SHA-256 fingerprint** (generated once; a
|
|
fresh cert each boot would invalidate every baked bootstrap pin). The fingerprint is what the
|
|
controller pins (leaf-cert SHA-256 — consistency with the agent's PVE/PBS pinning).
|
|
- **7 §6 endpoints**, all self-scoped: `GET /storage` (this guest's mpN mounts + fast/slow class),
|
|
`POST /snapshot`, `POST /rollback`, `POST /backup` (enqueued, crash-consistent — quiesce is 8B),
|
|
`GET /backup/due` (thin in 8A), `GET /backup/status`, `GET /restore-test/status`.
|
|
- **Token store**: durable, crash-safe per-guest token→guest map persisting only a **SHA-256 hash**
|
|
(plaintext exists transiently mint→write-to-mount, then discarded); last-write-wins; fsync'd JSONL.
|
|
- **Self-scoping**: VMID is resolved ONLY from the token; an explicit `vmid` that disagrees → **403,
|
|
op never issued for the other guest**; absent/unknown → 401.
|
|
- **`internal/provision`** — the back-half: mint per-guest token → render the stable `bootstrap.json`
|
|
(`schema felhom.bootstrap/v1`; **no registry credential** — controller image is golden-baked) →
|
|
write `0600` → **`chown 100000:100000`** (mapped guest-root, spike gotcha 1) → attach a read-only
|
|
bind mount via `pct set`. Host-side only (F3 — no `pct exec`). The token is never logged/returned.
|
|
- **`--selftest=provision`** — full chain on-demand (front half + back half; keeps the guest).
|
|
- **`config.LocalAPIConfig`** (`local_api`) — optional 6th daemon goroutine; disabled cleanly when
|
|
unconfigured or on a token-store/cert failure (the daemon still reports/reconciles).
|
|
- **`configs/build-golden.sh`** bakes the controller image (pulled once on the trusted build host,
|
|
then `docker logout` — no cred baked) + a controller-bootstrap unit that deploys the **baked** image
|
|
from the config mount on boot. Plus `configs/felhom-localapi-firewall.example` and a narrow
|
|
`FELHOM_PROVISION` sudoers alias.
|
|
|
|
## Design note — the back-half's host-root surface
|
|
|
|
The back-half's `chown` + bind-mount attach are host-root ops that the API token cannot do (bind
|
|
mounts are root@pam-only). They are **NOT** added to `proxmox.Privileged` (fenced to its 3
|
|
exceptions, "do not add"); they live in `internal/provision` and run through the shared `Runner`
|
|
(direct as root, or `sudo -n` with the new sudoers alias), confined to the agent-owned
|
|
`/var/lib/felhom-agent/guests/*` path. This is the per-guest provisioning host-root surface —
|
|
host-side and F3-compliant.
|
|
|
|
## Tests
|
|
|
|
`go test ./...` green; `-race` green on the build server (pure-Go parts). localapi: self-scoping
|
|
(cross-guest snapshot/rollback/backup → 403, op never issued; own-guest uses the token's VMID), 401
|
|
paths, `/storage` class mapping, `/backup` enqueue, thin `/backup/due`, status scoping; token store
|
|
persists only the hash (plaintext never on disk), last-write-wins, survives reopen, uniqueness; the
|
|
leaf fingerprint is stable across reload. provision: writes 0600 + chowns + attaches with the right
|
|
args; the **token never appears in the Result**; the cross-repo `bootstrap.json` key-set is pinned.
|
|
|
|
## Live validation (demo-felhom)
|
|
|
|
Built a **new golden with the controller v0.35.0 baked** (`build-golden.sh`). `--selftest=provision`
|
|
into guest 8201: front half up (fresh MAC, 13s) → back half minted the token, wrote `bootstrap.json`
|
|
(`0600`, `chown 100000:100000`, verified root:root 0600 inside the guest) and `pct set`-attached the
|
|
read-only mount (mp9). The golden's **baked unit deployed the controller (no registry pull)**; the
|
|
controller **ingested the bootstrap → came up CONFIGURED, not setup mode** → reached the **real**
|
|
local-API `GET /storage` → **200**, saw its mount. Self-scoping against the live server: own→200,
|
|
cross-guest `?vmid=9999`→**403**, bogus→401, absent→401. Firewall narrowing demonstrated (source
|
|
gating: an off-bridge stand-in blocked, the guest allowed). **Secret discipline**: token plaintext
|
|
absent from the agent log, store holds only the hash, bootstrap `0600`+chowned, selftest never prints
|
|
the token. The standing demo agent service was upgraded to **v0.10.0** (local-API live on
|
|
`192.168.0.162:8443`); the test guest was torn down.
|
|
|
|
## Deferred (stated, not built)
|
|
|
|
Quiesced app-consistent backup (`/backup/due` quiesce loop) → 8B. Controller de-privileging + customer
|
|
disk endpoints → 8C. The `BringUpSpec`/provision source (hub desired-state) + deterministic
|
|
attach-before-start sequencing → slice 10 (in 8A the back-half follows the front-half's start; the
|
|
golden bootstrap unit's `After=docker.service network-online.target` reliably sequences it after the
|
|
host-side mount attach). No secrets committed.
|