37b5ba08a7
gates / gates (push) Successful in 13s
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.
240 lines
14 KiB
Markdown
240 lines
14 KiB
Markdown
# 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.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**.
|
|
|
|
**§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-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.
|