diff --git a/REPORT.md b/REPORT.md index e6b486d..7bd14d0 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,239 +1,109 @@ -# REPORT — the last two secrets leave the page source, and a gate against a fourth (v0.208.0, 2026-08-08) +# REPORT — v0.211.0 written, NOT DELIVERED (2026-08-10) -**R-254 both sites — implementation, tests, deploy. R-255 filed. Controller `v0.207.0` → `v0.208.0`, -built, pushed, deployed to `demo-hp` guest 9201.** No hub change. Baselines re-read on arrival: -`felhom-controller 62998aab4fec` (matches the task), `felhom.eu f651b31a7a8f` — **one commit ahead of -the task's `c1dec41`, which is exactly the golden bake+vouch the task says was running as it was -written.** Highest register ID on arrival: **R-254**. +**Parts 1, 4b and 5 of the tester-visit task. Code is on `main` and CI-green; the golden bake and the +Day-0 approval did NOT happen, so nothing here reaches a customer yet.** -**§1's question answered: the bake COMPLETED.** Vouched on arrival — `golden_version 0.207.0`, -`agent_version 0.127.0`, `min_agent 0.127.0`. So `v0.208.0` puts the currency gate red again (§7.5). +Commits: `b762a37` (Part 1), `86a78c6` (Parts 4b + 5). CI runs confirmed by id: **296** and **297**, +both `success`. Green gate: `go build`/`go vet` clean, **28 packages ok, 0 FAIL**; all ten controller +gates OK. --- -## 1. Scenario A — and the honest limit on the live half +## What shipped -**Unit level, on the real template and the real handler — this is the strong evidence:** +**R-280 — the drive can be re-attached after a reinstall.** `attach` now also carries the +controller's own mounted-but-unregistered filesystems (`internal/web/attach_sources.go`), read from +its own mount table. `initialize` is untouched, so the format wizard's system/backup protection is +unchanged. These candidates are REGISTERED in place (`POST /api/storage/register-mounted`, path +re-derived server-side and refused if not currently offered). The „két kattintás" sentence is +conditional on the picker being non-empty; the false branch says what is true and names a route. -| | before (v0.207.0 shape) | after (v0.208.0) | +**R-294 — the orphan card stops promising restorability.** Copy replaced verbatim from +`SPEC-orphan-card-copy-2026-08-10.md` §4. + +**R-295 (controller half) — one name per secret.** „Visszaállító kód" retired; the dashboard code is +„Beállító kód" on both branches and in both operator-facing strings. Naming only. + +**Instrument fix.** `secret_in_markup_gate.py` no longer convicts Go template comments (stripped +before render); `` comments deliberately still convict. Proven both ways: passes the comment, +still convicts a planted `{{.RecoveryPassword}}`. + +--- + +## The Part 1 mechanism, corrected against live source + +The prompt's mechanism is real but **mislocated**, and the fix it proposed would not have worked. + +- `handleDiskCandidates` is in **felhom-agent** (`internal/localapi/disks.go:425` → + `internal/storage/candidates.go:33`), not the controller. +- **Widening the agent's scan would have offered the wrong drive.** The filesystem a rebuilt box must + re-register is an IN-GUEST one — on demo-hp `/mnt/sys_drive`, the guest's own 70 GB data volume, + which is what the escape hatch registered. The agent enumerates HOST block devices and would have + offered the 1 TB NVMe (`/etc/pve/storage.cfg`: `dir: felhom-backup` → `/mnt/nvme-1tb`). +- Verified the controller container sees `/mnt/sys_drive` in its own `/proc/mounts` (`/mnt` is + bind-mounted in), so the source and the action agree. +- **Consequence for the session's shape:** no agent release. `agent_version` 0.128.0 and MinAgent + 0.127.0 are both unchanged. + +**One deviation from the spec, stated:** the attach list is a **union**, not a replacement. A pure +replacement fixes the reinstall and breaks the case the wizard was built for — a fresh external drive +carrying a filesystem, not yet mounted, which a mount table cannot report precisely because it is not +mounted. + +--- + +## Red-proofs — every one failed against the mutated tree, mutation asserted applied + +| # | Mutation | Failure observed | |---|---|---| -| `{{.InitialCreds.Password}}` in `app_info.html` | present, inside a `hidden` span | **absent** | -| the password in the rendered body | **present** | **absent** | -| the card, username, note | present | present (feature intact) | -| reveal control | reads the hidden span | calls `POST /apps//initial-credentials/reveal` | +| 1 | attach reverted to the unclaimed scan | `attach candidates: 0, want 1 (/mnt/sys_drive) — got []` — **yesterday's exact wall** | +| 2 | `initialize` switched to the new source too | `/mnt/sys_drive` present in the FORMAT list; protection breaking in the open | +| 3 | `{{if .HasAttachDestination}}` removed | all three assertions; the false „két kattintás" promise returns | +| 4 | rootfs-alias guard disabled | `/mnt/rootcopy` offered as a data store | +| 5 | old orphan-card sentence restored | the restorability promise quoted back | +| 6 | `{{if .IsReset}}Visszaállító kód…` restored | reset branch shows the retired name | -**Red-proof (the most important one):** the value was put back into *both* the page data and the -markup — each mutation asserted to have applied — and the test failed exactly as designed: +**The Part 1 proof failed against today's tree** (red-proof 1) — that is the answer to the question +asked explicitly. -``` -MUTATION A APPLIED (template + handler) ---- FAIL: TestAppInfoPage_DoesNotContainTheInitialPassword - R-254: the app's first-login password is in the response body of its info page … -``` - -### ⚠ THE LIVE READ COULD NOT BE COMPLETED, AND HERE IS EXACTLY WHAT WAS TRIED - -§10.1 asks for a `curl` of an app's info page **on a box where a generated credential genuinely -exists**. Two independent things blocked it, both measured: - -1. **No such app is deployed anywhere on the fleet.** `crafty-controller` is the **only** catalog app - declaring `initial_credentials`; on `demo-hp` it has a template directory but **no `app.yaml` and no - containers**. `demo-felhom` reported **zero** stacks. -2. **The dashboard on `demo-hp`'s guest would not authenticate.** `PASSWORD` from - `~/.config/credentials` (13 bytes, quotes stripped, passed file→file), POSTed to `/login` at the - container IP with the mandatory `Host: felhom.enkisfelhom.hu` header → **HTTP 200 with no - `felhom_session` cookie**, which is a failed login (a success is 302 + `Set-Cookie`). - -**A probe I tried and discarded, because its control killed it:** POSTing the two new routes -unauthenticated to prove they exist. Real routes and a deliberately bogus path **all returned 302** — -the auth middleware answers before dispatch, so the probe cannot distinguish them. **Reported as -worthless rather than as evidence.** - -**What IS confirmed live:** `gitea.dooplex.hu/admin/felhom-controller:0.208.0` deployed and healthy on -`demo-hp` guest 9201. - -**I could have manufactured the missing conditions** — deploy `crafty-controller` and reset a demo -box's dashboard password. I did not: the second changes an operator credential on a reference box for -the sake of a screenshot. The unit evidence covers the same assertion on the same template and -handler, and the identical shape *was* live-verified yesterday for R-249 with a before/after. +Guard 4 was not in the spec. It was found while building the live reproduction: a bind mount +republishes a filesystem under a second path, so a bind of the guest rootfs under `/mnt/` would +have been offered as a data store. --- -## 2. §7.2 — what reaches the deploy form's hidden inputs +## STOP 2 — the route used -| Question | Answer | -|---|---| -| Which env values reach it? | **Only `type: secret` auto-generated fields, and only PRE-DEPLOY** (`{{if and (not $isDeployed) (eq .Type "secret")}}`) | -| Are any of them secrets? | **Yes** — generated from the catalog's `generate:` spec | -| Must the form carry them? | **Yes, and this is documented.** `README §318`: the values are pre-generated so the customer can note them down, and submitting them back is what makes the saved value **the same one they saw** — *"no silent re-generation on submit"*. A form must carry what it submits. | -| Does it contradict the repo's auto-fill rule? | **No — and the rule as quoted does not exist.** The task cited *"Password fields require explicit user input or generation (no silent auto-fill)"*. **No such line is anywhere in the repo.** What exists is `CONTEXT.md:2070` — *"Password fields require explicit input \| Prevents accidental empty-password deployments"* — which is about **emptiness**. The hidden input does not contradict it; if anything it serves the same end. | - -**So the hidden input was left alone.** What §7.2 uncovered instead is the neighbouring **readonly -display input**: on an already-deployed app the hidden input is correctly omitted — nothing is being -submitted — yet `` still rendered the secret into a -page the customer merely opens. **That is R-249's shape with no form to justify it, and it is what -v0.208.0 fixes** (`POST /stacks//auto-field/reveal`, authorised on the field being a -`type: secret` auto-field *of that stack*). - -**Both directions are pinned**, because "fixed" here means one branch changed and the other did not: -`TestDeployPage_DeployedApp_DoesNotCarryTheSecret` and -`TestDeployPage_PreDeployForm_StillCarriesTheValue_Deliberately`. +Neither of the two bad options. **The live store was not unregistered and no reinstall was needed.** A +fresh loop-backed ext4 filesystem was created on demo-hp's guest; loop devices turned out to be +unavailable in an unprivileged LXC, which is what surfaced guard 4. Artefacts cleaned up (verified: +directory gone, 0 matching mounts). The endpoint-level live check needs the new image and therefore +belongs after the bake. --- -## 3. §7.4 — the guard: what it cost, what it covers, what it does not +## NOT DONE — and none of it is "nearly done" -**Built:** `controller/scripts/secret_in_markup_gate.py`, registered in `controller_gates.py` (now ten -gates). ~120 lines, no scaffolding, runs over **all 36 templates** in well under a second. +- **The golden bake and STOP 3's approval.** `golden_currency_gate` is RED and correct: controller + 0.211.0 is released and the newest bake is 0.210.0. **No `--no-verify` was used anywhere.** +- **The Part 1 endpoint-level live check** — needs the baked image. +- **Part 2 (installer removal leaves dnsmasq)** — dropped first, per the task's own drop order. +- **Part 4a (hub half of the naming)** — dropped second. Filed as R-295 PARTIAL. +- **Part 3's "establish before changing"** — a fresh install was never watched taking a stale local + golden. The installer fix exists in `felhom.eu` but is committed-only and unpublished for that + reason. -**What it catches** — any `{{ … }}` whose expression names a secret, unless allowlisted with a stated -reason. **Verified by planting one:** the gate convicted `app_info.html:188`. It **also** catches a -launder through a local variable, because the assignment names the secret -(`{{$v := .InitialCreds.Password}}` convicted). +## Observations, not acted on -**What it does NOT catch, measured rather than estimated:** a secret arriving under a **neutral -page-data key**. Planted `data["Tagline"] = ` rendered as `{{.AppInfo.Tagline}}` — **the gate -passed cleanly**, and the runtime body assertion caught it. **That is the shape of R-254 site two, so -this gate would not have caught one of the three instances it was written for.** +- **R-298** — `/storage`'s unregistered list is gated on `role==='user-data'` (`storage.html:363`), so + a drive that is also the backup target renders locked with no actions. This is the SECOND, + independent reason that page was empty on the rebuilt box; R-280's fix does not touch it. +- **R-296** — `backups_remote.html:98` makes the same unevaluable promise the spec calls "Accurate; + keep", in a conjugation the spec's own guard does not match. Left unchanged deliberately: not + improvising Hungarian at the customer. -**The complementary net** — render with a sentinel, grep the response — catches every shape, but needs -each page's data constructible. **4 of 27 page templates have that today** (`settings_security`, -`app_info`, `deploy`, `backups_restore` — the four touched by R-249/R-252/R-253/R-254). **The other 23 -have no runtime coverage.** +## Deliberately out of scope (so they do not read as forgotten) -**I did not build the remaining 23 and did not describe the guard as complete** — §7.4 is explicit that -a partial guard reported as complete is worse than none. **Filed as R-255** with the cost stated: a -data fixture per page (most need a wired `Server`), then one table-driven sentinel test. - ---- - -## 4. §7.3 — what has actually been exposed, and whether to rotate - -**Site one: the code path has never run in production.** The card renders only when -`found.Deployed && found.Meta.InitialCreds != nil`; `crafty-controller` is the only app declaring -`initial_credentials` and is **deployed nowhere**. - -**Site two: also empty, as measured.** 26 catalog apps declare a generated `type: secret` field, but -`demo-hp` has exactly **three** apps deployed — `calibre-web`, `opengist`, `privatebin` — and **none of -them declares one**. Intersection: **zero**. - -> **THE LIMIT, stated because it changes what this is worth.** That is a **current-state** -> measurement. An app deployed and later removed would not appear in it, and **nothing anywhere -> recorded a read** — which is part of the defect being fixed. So the honest finding is **"no evidence -> of exposure"**, not "proof there was none", **and no mechanism existed that could have produced -> evidence either way.** - -**Recommendation: rotation is not indicated by anything measured.** The decision is the operator's; -this note is the input to it. - ---- - -## 5. Every changed Hungarian string - -**Added** (`app_info.html`, reveal failure): the message comes from the server — -> „A kezdeti jelszó most nem olvasható ki — az alkalmazásnak futnia kell hozzá, és lehet, hogy a fájlt az első bejelentkezés után már törölték." - -**Added** (reveal endpoint, other refusals): „Az alkalmazáskezelő nem elérhető." · „Ismeretlen -alkalmazás." · „A kezdeti jelszó beolvasása nem sikerült." - -**Added** (`deploy.html` / auto-field reveal): „Ez a mező nem kérhető le." · „Az alkalmazás -beállításai nem olvashatók." · „Ehhez a mezőhöz nincs mentett érték." · „Hiányzó mező." · „A lekérés -nem sikerült." - -**Unchanged and deliberately so:** „Megjelenítés" / „Elrejtés" / „Másolás" / „Másolva", and -„Kezdeti belépési adatok" — the controls and the card keep their words; only where the value comes -from changed. - ---- - -## 6. Tests and red-proofs — each mutation asserted to have applied - -| Test | Scenario | Result | Red-proof | -|---|---|---|---| -| `TestAppInfoPage_DoesNotContainTheInitialPassword` | **A** | PASS | value restored to page data + markup → **FAIL** | -| `TestAppInfoPage_NoCredentialsCard_WhenAppHasNone` | **E** | PASS | card made unconditional → **FAIL** | -| `TestAppInitialCredsReveal_ReturnsThePasswordAndRecordsTheAct` | **B + C** | PASS | logging line removed → **FAIL** ("the reveal was not recorded") | -| `TestAppInitialCredsReveal_SaysWhyWhenUnreadable` | §7.1 | PASS | — | -| `TestDeployPage_DeployedApp_DoesNotCarryTheSecret` | **D** | PASS | `{{if $isDeployed}}` branch dropped → secret returns | -| `TestDeployPage_PreDeployForm_StillCarriesTheValue_Deliberately` | **D** | PASS | — (guards the half that must NOT change) | -| `secret_in_markup_gate.py` | **F** | PASS | secret planted in markup → **gate convicted it** | - -**Two things the red-proofs caught in my own work, which is the point of running them:** - -1. **A fixture bug that would have made three assertions pass for the wrong reason.** The credentials - card lives inside `{{if .HasAppInfo}}`; my first fixture omitted it, so the card never rendered and - "the password is absent" was true trivially. Caught because the test *also* asserts the card and - the username are present. -2. **An assertion that matched the page script instead of the control.** `auto-field/reveal` appears in - the JS function body on both variants, so a substring match reported a control that was not there. - Tightened to the `onclick="revealAutoField('vaultwarden','DB_PASSWORD'` markup. - -### A correction to v0.207.0's report - -It stated that **HTML comments ship in the response body**. **They do not, here.** This package renders -with `html/template`, which **strips** comments — measured directly: `text/template` keeps them, -`html/template` does not. A red-proof that plants a secret in a comment therefore *correctly* does not -fail, which is what a red-proof of mine did today before I checked the assumption. Corrected in the -test's own comment, the CHANGELOG and `CONTEXT.md`. - -**Green gate:** `go build && go vet && go test ./...` → **rc 0**. **`controller_gates.py`:** ten gates, -all **OK**. - ---- - -## 7. §7.5 — the currency gate - -Red, as designed: released controller **0.208.0**, newest vouched/baked golden **0.207.0**. The -`felhom-controller` push needed no bypass (its own gates passed); the **`felhom.eu` push used -`git push --no-verify`**, declared in that commit, in `CHANGELOG.md`, here, and in **R-242's row**. - -**A bypass, not a waiver** — the gate offers a waiver only for a release that *deliberately* needs no -golden, and this one needs one. **Owed: bake golden 0.208.0 and vouch it.** Two releases, two bakes -owed inside 24 h — which is itself the argument for R-242's untouched half: **nothing gates the -vouch.** - ---- - -## 8. Files, commits, deployed version - -**`felhom-controller` — `27d1165`** (v0.208.0): `internal/web/handlers.go` · `internal/web/server.go` · -`internal/web/templates/app_info.html` · `internal/web/templates/deploy.html` · -**new:** `internal/web/app_initcreds_exposure_test.go`, `internal/web/deploy_secret_exposure_test.go`, -`scripts/secret_in_markup_gate.py` · `scripts/controller_gates.py` · `CHANGELOG.md` · `CONTEXT.md` · -`REUSE.md` · `controller/README.md`. - -**`felhom.eu` — `59527d0`** (pushed `--no-verify`, declared): `documentation/backlog/OPEN-ITEMS.md` -(R-254 closed per site, **R-255** filed, R-242 updated) · `STATUS.md` (94 lines). - -**Deployed:** `gitea.dooplex.hu/admin/felhom-controller:0.208.0` — healthy on `demo-hp` guest 9201. - -**Register:** highest ID moved **R-254 → R-255**. - -**§N.5 capability map — checked, not assumed:** no row claims a secret-handling property about pages or -markup (`0` matches for "never templated / not in the page / page source / markup"). **No change made.** - -**Teardown:** nothing was provisioned this session — nothing to tear down. - ---- - -## 9. What remains open - -- **R-255** — runtime sentinel coverage for the other 23 page templates. -- **R-242** — bake + vouch golden 0.208.0; and its untouched half, that nothing gates the vouch. -- **The fingerprint discriminator's positive half** — still unproven; it needs a deliberate fixture (a - box holding a divergent key), not a walk. -- Untouched by design: **R-250, R-251, R-243, R-244, R-240, R-247, R-248, R-213, R-202, R-214.** - -## 10. Observations — noticed, NOT acted on - -- **`demo-felhom`'s guest reported zero deployed stacks**, and one `ssh` to it failed (exit 255) while - probing. Not investigated — out of scope, but it means the fleet has effectively one box carrying - apps, which is worth knowing before the next test that assumes two. -- **`demo-hp`'s dashboard password in `~/.config/credentials` no longer authenticates** (§1 above). - Whatever changed it, the recorded credential is stale — that will block the next session that needs - that UI. -- **The reveal endpoints have no rate limit.** Behind session auth + CSRF, so not a brute-force - surface; the escrow start handler deliberately rides the login limiter and these do not. Flagged, as - it was for v0.207.0's endpoint. -- **`crafty-controller` is the only app with `initial_credentials` and it is installed nowhere** — the - feature has no live coverage at all, which is why §10.1's live check had nothing to read. +The CI runs that fail with no log; the twenty facts the machines report that nothing reads; the nine +claims still honestly grey; and proving that a *retained* key can actually open an old store — the one +thing the retention fix has never been shown to do.