diff --git a/REPORT.md b/REPORT.md index 62010ea..c477dc1 100644 --- a/REPORT.md +++ b/REPORT.md @@ -2,45 +2,65 @@ > **Overwrite** this file with a summary of the most recent task only (uniform with the other repos; not cumulative). The cumulative hub history lives in [hub/CHANGELOG.md](hub/CHANGELOG.md). -## hub v0.15.0 — Phase 2 managed updates: per-customer controller-version floor (2026-06-27) +## BUNDLE slice — Day-0 artifact manifest + self-install the agent (2026-06-28) -**Why.** Phase 1 gave enthusiasts an opt-in "update to latest" button. Phase 2 adds the managed -default: the operator sets a per-customer **minimum controller version** (defaulting to a global floor), -and any box below it **auto-updates** on its next report — so the household that never clicks stays -current and patched. Floor = the operator's enforced minimum and the auto-target; latest stays the -customer's manual opt-in. +**Hub v0.15.0 → v0.16.0** @ `39ef64e`/`d5266ca` · **host-install v1.0.0 → v1.1.0** @ `30ecf73` -### Files changed -- `internal/store/store.go` — `customer_configs.min_controller_version` column; `hub_settings` KV table; - `defaultMinControllerVersion` + `SetDefaultMinControllerVersion`; `Get/SetGlobalMinControllerVersion`, - `SetMinControllerVersion`, `EffectiveMinControllerVersion`. -- `internal/api/handler.go` — `LatestVersionProvider` interface + `SetLatestVersionProvider`; report ACK - returns `{min_controller_version, latest_version}` (both omitted when empty). -- `internal/web/configs.go`, `server.go`, `templates/configs.html`, `templates/customer_unified.html` — - operator UI: global floor editor + per-customer override form + Floor column / below-floor markers; - routes `POST /configs/global-floor`, `POST /customers/{id}/floor`. -- `cmd/hub/main.go` — `Config.ControllerUpdates.DefaultMinVersion` (yaml `default_min_version`, env - `DEFAULT_MIN_CONTROLLER_VERSION`); seeds the store default; wires the version checker as latest provider. +The host-bootstrap script now **installs the agent itself** (the last manual Day-0 prerequisite is +gone). It fetches the agent binary + golden from Gitea generic packages and **verifies each artifact's +sha256 against a hub-vouched manifest** before installing/using it. The hub is the checksum **trust +root** (a different root than Gitea, which only stores the bytes); the fetch credential is the git +token already inside the customer's `controller.yaml` (config-retrieve) — **no new credential**. -### Design -- Effective floor = per-customer override if non-empty, else global (hub_settings → config/env default), - else "" (Phase 2 inert for that customer). -- Rides the **existing report cycle** — no new floor-polling endpoint. The actual swap is the - controller's Phase 1 flow (in-guest pull → agent `POST /controller/swap` → rollback); the hub only - advertises the floor. **No agent change.** +### Hub (v0.16.0) +- `store.go`: `ArtifactManifest{agent,golden version+sha256}` in the existing `hub_settings` table; + `Get/SetArtifactManifest` + generic `getSetting/setSetting`. +- `handler.go`: `GET /api/v1/artifacts/{customer_id}` — auth mirrors `handleConfigRetrieve` + (`X-Retrieval-Password`, 404-then-401, constant-time). Unset manifest → 200 with empty fields. +- `configs.go` + `configs.html`: operator UI "Day-0 artifacts" card (`POST /configs/artifacts`, + semver + 64-hex validation). `main.go`: env-seed (`ARTIFACT_AGENT_VERSION`/`…_SHA256`/`GOLDEN_…`), + empty-fields-only so a UI edit sticks (the operator UI is password-gated — same escape hatch as the + Phase-2 floor). +- Tests (`artifact_test.go`): returned-verbatim / unset-empty / 401 / 404 / store partial round-trip. -### Tests (`go test ./...` green) -- `internal/store/floor_test.go` — empty-when-unset; global-default fallback; DB-overrides-default; - **override-beats-global (companion red-proof)**; set-and-preserve-across-save. -- `internal/api/floor_ack_test.go` — ACK carries effective floor (override beats global) + latest; omits - both when unset. -- `internal/web/render_test.go` — template render smoke (catches `template.Must` panics + floor fields). -- **Companion red-proof (verified):** making `EffectiveMinControllerVersion` ignore the override → - `TestEffectiveFloor_OverrideBeatsGlobal` AND `TestReportACK_FloorAndLatest` FAIL → restored → green. +### host-install (v1.1.0) +- New **step 5/8 agent-install**: resolve manifest + git token → fetch binary → **verify sha256 vs hub + manifest** (abort on mismatch) → install the non-root `felhom-agent` user + binary + sudoers + (`visudo -cf`-validated) + canonical unit. Installs the `sudo` package (the non-root model needs it). +- New **step 7/8 golden**: local fallback else fetch+verify+import from Gitea (`--force-gitea-golden`). +- Agent now runs **non-root** (`privileged.mode: "sudo"`); config chowned to the service user. +- `--skip-provision` (agent-only install/verify), pre-flight relaxed (binary no longer required), + README prerequisites trimmed to: install PVE + create the customer. ### Green gate -`go build ./... && go vet ./... && go test ./...` → all pass. +Hub: `go build/vet/test ./...` green. Scripts: `bash -n` + `shellcheck -S warning` clean. -### Deploy -Built + pushed `gitea.dooplex.hu/admin/felhom-hub:0.15.0`, manifest bumped + synced (see hub/CHANGELOG -and the live-test summary in felhom-controller/REPORT.md for the end-to-end floor auto-update result). +### Deployed + live-proven (felhom-pve, demo-felhom) +- Hub **0.16.0** rolled out to k3s (`felhom-system/hub`); artifact manifest seeded via env. Endpoint + live: correct passphrase returns the manifest; **wrong/missing → 401, unknown customer → 404**. +- Published **agent 0.43.0** (sha `8b989917…`) + **golden 0.85.1** (sha `f87031cc…`) to Gitea generic. +- **From-scratch agent install** (live agent moved fully aside): fetch → **verify sha256** → install + (non-root user + binary + sudoers + unit + config) → service **active as `felhom-agent`** → + `--selftest=hub` host-report landed → **9201 still managed**. The git token (from `controller.yaml`) + reads Gitea generic packages (confirmed PUT/GET/DELETE). +- **sha256 negative:** a corrupted published binary made the install **abort** ("Refusing to install — + verify-before-use", exit 1); the good binary was then restored (sha re-verified). +- **Golden fetch+verify+import** from Gitea proven; the test-imported copy was cleaned up. + +### Two real bugs found + fixed live +1. The `sudo` package wasn't installed (the host ran the agent root+`direct`) — step 5 now installs it. +2. `felhom-agent.sudoers` had **unescaped commas** in the `lvs`/`lsblk -o` lists → `visudo -cf` + rejected it (never validated live before). Fixed in felhom-agent (escaped commas). + +### Observations +- **Throwaway full provision SKIPPED** to protect the live demo: a guest provisioned with + demo-felhom's config would fight the live 9201 for the Cloudflare tunnel/domain. The new code (agent + install + golden fetch/verify/import) is fully proven; the golden→guest restore is unchanged from + prior slices. Use a dedicated throwaway customer for an end-to-end provision proof. +- **Non-root + PBS:** the agent can't read `/etc/pve/priv` as `felhom-agent` (PBS datastores skipped + with a WARN; cadence disabled on the demo). Needs a pmxcfs group/ACL or sudoers grant for PBS in + production — follow-up. +- **Per-customer artifact pinning** is a future hook (the endpoint already takes the customer id; + v0.16.0 returns the global current set). +- felhom-pve left with a **healthy non-root agent 0.43.0** + 9201 managed; rollback bundle kept at + `/root/agent-backup-bundle-test`. diff --git a/documentation/audits/SPIKE-day0-firstboot-handshake-2026-06-26.md b/documentation/audits/SPIKE-day0-firstboot-handshake-2026-06-26.md index 0bdb1a0..e3b2a54 100644 --- a/documentation/audits/SPIKE-day0-firstboot-handshake-2026-06-26.md +++ b/documentation/audits/SPIKE-day0-firstboot-handshake-2026-06-26.md @@ -1,5 +1,15 @@ # SPIKE — Day-0 first-boot trust handshake (operator-deploy) +> **Update 2026-06-28 (BUNDLE slice):** Day-0 now **self-installs the agent** + fetches the golden from +> Gitea. The host-bootstrap script (`scripts/felhom-host-install.sh` v1.1.0) fetches the agent binary + +> golden from Gitea generic packages and **verifies each sha256 against a hub-vouched artifact manifest** +> (`GET /api/v1/artifacts/{id}`, hub v0.16.0) before installing/using them — the fetch credential is the +> existing config-retrieve git token (no new credential); the checksum trust root is the **hub**, not +> Gitea. The "works on a box that isn't felhom-pve" gap is closed: prerequisites are now just **install +> PVE + create the customer in the hub**. The agent runs **non-root** (`felhom-agent` + sudoers). +> Remaining follow-ups: per-customer artifact pinning, a `make golden` that resolves-latest, non-root +> PBS-key access, the DR mode, and the local-DNS slice. + **Date:** 2026-06-26 **Class:** Spike (empirical validation; no production code shipped). Output is this doc only. **Question:** Does the composed first-boot chain — customer-in-hub → host mint → agent host-auth →