slice 8A (agent half): local-API server + provisioning back-half (v0.10.0)

internal/localapi: per-guest local-API server (doc 03 §6) — 7 self-scoped
endpoints, hashed per-guest token store, persisted self-signed leaf with stable
SHA-256 pin, optional 6th daemon goroutine. internal/provision: back-half —
mint token, render bootstrap.json (no registry cred), write 0600, chown
100000:100000, attach pct-set bind mount (host-side, F3, no pct exec).
--selftest=provision. build-golden.sh bakes the controller image + bootstrap
unit. sudoers FELHOM_PROVISION; firewall narrowing artifact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 09:47:42 +02:00
parent fae11020a5
commit 3fecf4c713
18 changed files with 2203 additions and 11 deletions
+57
View File
@@ -3,6 +3,63 @@
All notable changes to **felhom-agent** are recorded here. Update on every code
change that gets pushed.
## v0.10.0 — slice 8A: agent local-API server + provisioning back-half (2026-06-10)
The host-agent half of slice 8A (doc 03 §6). Adds the per-guest **local API** the in-guest
controller calls over the bridge, and the **provisioning back-half** that follows the slice-7
bring-up front half. Grounded by `felhom.eu/documentation/tests/slice8a-channel-deploy-spike-findings.md`
(commit `4a81a96` — channel + deploy plumbing proven; the 5 gotchas resolved here). Controller half
is `felhom-controller` v0.35.0. No hub change.
### Added
- **`internal/localapi`** — the HTTPS local-API server (doc 03 §6), 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 **7 §6 endpoints**,
all **self-scoped to the caller's own guest**: `GET /storage` (this guest's mpN mounts + fast/slow
class from the slice-5/7 storage view), `POST /snapshot`, `POST /rollback`, `POST /backup`
(enqueued, crash-consistent — the app-consistent quiesce loop is 8B), `GET /backup/due` (thin in
8A), `GET /backup/status`, `GET /restore-test/status`.
- **Token store** (`tokenstore.go`): durable, crash-safe per-guest token→guest map that persists
only a **SHA-256 hash** of each token (the plaintext exists transiently at mint→write-to-mount,
then is discarded), last-write-wins per guest, fsync'd append-only JSONL (mirrors the nonce store).
- **Self-scoping**: the VMID is resolved ONLY from the token; an explicit `vmid` (query/body) that
disagrees → **403 and the proxmox op is never issued for the other guest**; absent/unknown → 401.
- **`internal/provision`** — the back-half: mint the per-guest token → render the stable
**`bootstrap.json`** contract (schema `felhom.bootstrap/v1`; **no registry credential** — the
controller image is baked into the golden) → write it `0600`**`chown 100000:100000`** (the
unprivileged-LXC mapped guest-root, spike gotcha 1) → attach a **read-only bind mount** via
`pct set`. Host-side only (F3 — the agent never enters the guest; **no `pct exec`**). The token
plaintext is never logged and never returned.
- **`--selftest=provision`** — the full chain on-demand: bring-up (provision) front half + the
back half; keeps the guest for the golden's baked controller-bootstrap unit to deploy.
- **`config.LocalAPIConfig`** (`local_api`) — enable + bridge `listen_addr` + cert/key paths + token
store path. The server is an optional 6th daemon goroutine, disabled cleanly when unconfigured or
on a token-store/cert failure (the daemon still reports/reconciles).
- **`configs/build-golden.sh`** now **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 (no login/pull at deploy).
- **`configs/felhom-localapi-firewall.example`** — host firewall narrowing of the local-API port to
the guest bridge subnet (nft/iptables/PVE variants; defense-in-depth — the token stays the gate).
- **`configs/felhom-agent.sudoers`** — a narrow `FELHOM_PROVISION` alias (`chown 100000:100000` +
`pct set` bind-mount, both confined to the agent-owned `/var/lib/felhom-agent/guests/*` path) for
the non-root least-privilege deployment.
### Security / design notes
- The local-API leaf is pinned by **leaf-cert SHA-256** (decision: consistency with the agent's
PVE/PBS pinning); the fingerprint is baked into each guest's bootstrap.
- The back-half's host-root ops (chown + bind-mount attach) are **NOT** added to `proxmox.Privileged`
(which is fenced to its 3 exceptions) — they live in `internal/provision` and run through the shared
`Runner` (direct as root, or `sudo -n` with the new sudoers alias). This is the per-guest
provisioning host-root surface, host-side and F3-compliant.
### Tests
- localapi: self-scoping (cross-guest snapshot/rollback/backup → 403, op never issued for the other
guest; own-guest uses the token's VMID), 401 paths, `/storage` class mapping, `/backup` enqueue,
the thin `/backup/due`, status scoping; the 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 the bind mount with the right args; the **token never
appears in the Result**; the cross-repo `bootstrap.json` contract key-set is pinned.
## v0.9.0 — slice 7 close-out: PBS recovery-code escrow creation (2026-06-10)
The first code that touches the PBS client encryption key `K` and introduces the customer recovery