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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user