Files
felhom-controller/REPORT.md
T
admin 37b5ba08a7
gates / gates (push) Successful in 13s
REPORT: v0.208.0 — both R-254 sites, the guard's measured holes, and what the live read could not prove
Records the deliverables, and is explicit about the limit on the live half: the
curl of an app info page could not be done, and names exactly what was tried —
crafty-controller is the only app declaring initial_credentials and is deployed
nowhere, and demo-hp's dashboard password in ~/.config/credentials no longer
authenticates (200 with no session cookie). A probe of the new routes was
discarded because its control killed it: real and bogus paths both 302 behind the
auth middleware.

§7.2's answer including the part that contradicts the task's premise: no line in
the repo says 'no silent auto-fill'; the rule is CONTEXT.md:2070 about accidental
EMPTY-password deployments. The hidden input is deliberate and untouched.

§7.4's measurement: the gate covers all 36 templates and catches a launder through
a local variable, but is blind to a secret under a neutral page-data key — the
exact shape of site two. Runtime coverage is 4 of 27 pages. Filed as R-255 rather
than described as complete.

§7.3: no evidence of actual exposure on the fleet, with the limit stated — it is a
current-state measurement and nothing recorded reads, which was part of the fault.

Also corrects v0.207.0's report: html/template STRIPS HTML comments; they do not
ship in the response body. Measured.
2026-08-07 21:29:13 +02:00

14 KiB

REPORT — the last two secrets leave the page source, and a gate against a fourth (v0.208.0, 2026-08-08)

R-254 both sites — implementation, tests, deploy. R-255 filed. Controller v0.207.0v0.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 f651b31a7a8fone 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.

§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).


1. Scenario A — and the honest limit on the live half

Unit level, on the real template and the real handler — this is the strong evidence:

before (v0.207.0 shape) after (v0.208.0)
{{.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/<slug>/initial-credentials/reveal

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:

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.


2. §7.2 — what reaches the deploy form's hidden inputs

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 <input type="password" value="{{$val}}" readonly> 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/<name>/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.


3. §7.4 — the guard: what it cost, what it covers, what it does not

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.

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).

What it does NOT catch, measured rather than estimated: a secret arriving under a neutral page-data key. Planted data["Tagline"] = <secret> 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.

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.

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-controller27d1165 (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.eu59527d0 (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.