Files
felhom-controller/REPORT.md
T
admin f3146ac7bf controller v0.73.0: DR recipe — emit secret-free customer+apps half in hub report
DR recipe slice (controller half), grounded in SPIKE-dr-recipe-2026-06-16. The
controller emitter is the BOUNDARY enforcement point: v1 ships an explicit
allowlist {catalog_ref, enabled, storage_bindings} and reads NOTHING from
AppConfig.Env, so no ENC:/token/password can leak. storage_bindings parsed from
the compose (${HDD_PATH}/${USERDATA_PATH} volume binds -> {container_path,
drive, subpath}).

Load-bearing tests: TestBuildAppRecipe_NoSecrets (synthetic-secret app -> none
leak) + TestBuildAppRecipe_AllowlistIsLoadBearing (red-proof companion) +
TestAppStorageBindings + TestBuildDRRecipeAppHalf. Red-proofed live: forcing the
emitter to dump Env makes the boundary test fail. recipe_version=1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 18:38:59 +02:00

3.3 KiB

REPORT — controller v0.73.0: DR recipe (customer + apps half)

TASK — DR recipe slice (controller half). Emit the secret-free customer + apps half of the reconstruction recipe as an additive hub-report section. Grounded in SPIKE-dr-recipe-2026-06-16.md. The controller emitter is the boundary enforcement point — it is the component that distinguishes secret from non-secret deploy fields, so v1 ships only an allowlist and the no-secrets boundary test lives here.

Implementation

  • internal/report/dr_recipe.go:
    • DRRecipeAppHalf{recipe_version, customer{id,display,domain}, apps[]}.
    • BuildDRRecipeAppHalf(custID, custName, domain, stacks, composeReader) — pure given the reader; one AppRecipe per DEPLOYED, non-protected stack.
    • AppRecipe{catalog_ref, enabled, storage_bindings} — the entire v1 surface. buildAppRecipe reads NOTHING from AppConfig.Env.
    • appStorageBindings(composeYAML, hddPath) — pure compose parser; each ${HDD_PATH}/${USERDATA_PATH} volume bind → {container_path, drive=basename(HDD_PATH), subpath}; named volumes excluded.
    • readComposeFile — the production reader (best-effort; unreadable → no bindings, never a failure).
  • Report.DRRecipe *DRRecipeAppHalf wired into BuildReport (customer fields + GetStacks()).

The boundary (the Phase-1 lesson)

The recipe carries ONLY {catalog_ref, enabled, storage_bindings} — identifiers/paths. It NEVER touches AppConfig.Env, where the controller keeps ENC: secrets. This is an allowlist (a new field is excluded by default), the inverse of the retired infra-backup that shipped restic_password / cf_api_token. Secrets stay in the PBS whole-CT snapshot + escrow, recovered with R, never here.

Tests (the load-bearing boundary test + companion)

  • TestBuildAppRecipe_NoSecrets — emit a recipe for an app whose Env carries an ENC: value + a token-shaped value; assert NONE of the values and NO credential-shaped key survive; assert the allowlisted facts DID emit (non-vacuous).
  • TestBuildAppRecipe_AllowlistIsLoadBearing — the companion/red-proof: a guard-removed shape leaks the token; the production emitter does not.
  • TestAppStorageBindings (+ _NoHDD) — pins the compose parse (roms + resources bindings; named volume excluded; rootfs app → 0 bindings).
  • TestBuildDRRecipeAppHalf — assemble-correctness (deployed + non-protected only) + whole-half secret sweep.
  • Live red-proof: forcing buildAppRecipe to dump Env made TestBuildAppRecipe_NoSecrets and the companion FAIL (caught the token value + the DB_PASSWORD/SECRET_KEY/IGDB_CLIENT_SECRET keys); reverted → green.

Versioning

recipe_version=1; read is ignore-unknown for forward-compat. The hub assembles this half with the agent's storage/guest/PBS half (agent v0.38.0) into one customer recipe.

Gate / deploy

go build, go vet, go test ./... all green (local + build server). Built + pushed image v0.73.0; deployed to guest 9201 (bootstrap-managed).

Deferred (NOT in this slice)

Free-form non-secret deploy fields (the SPIKE's non_secret_deploy_fields) — v1 is the three-field allowlist only; additional fields land incrementally behind the same allowlist test. No re-provisioning automation; no recovery-mode consumption.