Files
felhom-controller/REPORT.md
T
admin 62998aab4f
gates / gates (push) Successful in 18s
REPORT: v0.207.0 — R-249 before/after on a live box, the census, and what was NOT proven live
Records the deliverables: the raw response body before (1 occurrence, v0.206.0)
and after (0, v0.207.0) with a positive control in both directions; the §7.1
census finding two more instances of the render-then-hide pattern (R-254, one a
real per-install secret); §7.2's decision and why the promise was the wrong half;
every changed Hungarian string; all eight tests with their red-proof outcomes.

States plainly what was NOT proven live: R-252/R-253's notices could not be
rendered on VM 325 because both states are rebuild-only and the box re-registers
a drive on restart — the live run therefore exercised Scenario E instead, and the
notices are pinned at the template + predicate level with red-proofs.

Also records that red-proof D caught a fault in my own work: the explanatory HTML
comment quoted the old sentence, and HTML comments ship in the response body, so
the contradiction was still on the page and the assertion forbidding it could
never fail.
2026-08-07 18:33:37 +02:00

14 KiB

REPORT — a secret leaves the page source, and two refusals learn to say what to do (v0.207.0, 2026-08-08)

R-249, R-252, R-253 — implementation. R-201's record corrected, R-254 filed. Controller v0.206.0v0.207.0, built, pushed and deployed to demo-hp guest 9201 and VM 325. No hub change was needed. Baselines re-read on arrival and all four repos matched the task exactly (felhom-controller 3d3b4496f377, felhom.eu 3f4fb3825f06); highest register ID was R-253.


1. Scenario A — the raw response body, before and after

This is the deliverable that matters, and it was measured on a live box with a positive control in both directions. A known passphrase was planted on VM 325 (a throwaway due for destruction the same hour) and the security settings page fetched exactly as the defect was originally found — with curl.

v0.206.0 (the shipped defect) v0.207.0 (the fix)
page bytes 50 208 51 115
positive control — card renders (a passphrase IS stored) 1 1
reveal control present 2
passphrase occurrences in the response body 1 0

The positive control is load-bearing. The first attempt at this check returned 0 on a box that had no passphrase stored at all — a zero that meant nothing. That is why the card's presence is asserted first: an empty listing is not evidence of emptiness.

Scenario B, live: POST /settings/retrieval-password/reveal200, Cache-Control: no-store, body 85 bytes carrying the correct passphrase (1 match against the planted value). The controller logged retrieval passphrase revealed via the security page from 192.168.0.100 (value never logged), and a grep of five minutes of controller log for the value itself returned 0.

The red-proof — the most important one here

Both halves of the fix were reverted (data["RetrievalPassword"] = … restored in handlers.go, and {{.RetrievalPassword}} put back inside a display:none span), each mutation asserted to have applied before running:

BOTH MUTATIONS APPLIED — verified by string change
--- FAIL: TestSecurityPage_DoesNotContainTheRetrievalPassphrase (0.08s)
    R-249: the retrieval passphrase is in the response body of the security page …

Restored; residue checked ({{.RetrievalPassword}} occurrences: 0); green again.

Why the tests assert the body and not a view: the old markup hid the value with CSS. Every test that asked what does the customer see passed while the bytes carried the secret. A test that cannot see a display:none span cannot see this defect at all.


2. §7.1's census — the pattern found once, looked for everywhere

It was worth doing: two more instances, one with a real secret.

Where What Verdict
app_info.html:185 an app's auto-generated first-login password in <span … hidden>{{.InitialCreds.Password}}</span> beside a „Megjelenítés" button the same defect with a real per-install secretReadInitialCredentials reads it live out of the deployed container, so it is not the catalog's published default_creds
deploy.html:482 an auto-generated type: secret field in <input type="password" … value="{{$val}}"> weaker — the pre-deploy form must post the value; on an already-deployed app's page the exposure is gratuitous

Filed as R-254, not fixed — scope was R-249/R-252/R-253, and each needs its own reveal endpoint and its own body-asserting test rather than a shared quick edit. Recommended next, because R-249 proved the pattern is not theoretical.

Rotation advice, left to the operator: the walk5 instance is compromised (it reached a transcript) and dies with that customer's teardown. No other instance is known to have been read — but nothing records a read, which is itself part of the defect, and that is the honest state of the evidence. If any customer page has been screen-shared, saved or proxied, treat that passphrase as exposed.


3. §7.2's decision — which half of the reinstall promise was wrong

The promise was wrong, and the reason is structural rather than a matter of effort.

reconstituteOffbox refuses when GetStackHDDPath(stack) is empty, because the destination of a restore is the app's own data path — a drive the CUSTOMER chooses at deploy time. An automatic reinstall would mean the product picking that drive for them, which is precisely the decision this recovery path exists to leave with them (DOMAIN and SUBDOMAIN would have to be invented too).

So the smaller change is also the correct one: the copy now says to install first and routes there. The row's own code comment already read "restore in place vs. reinstall first" — the behaviour was right and the sentence had drifted.


4. Every changed Hungarian string, for review as copy

Removed (backups_restore.html) — the promise the restore could not keep:

„Nincs telepítve — a visszaállítás előbb újratelepíti."

Added (backups_restore.html, the not-installed row):

„Nincs telepítve — előbb telepítsd újra, utána hozhatod vissza az adatait." („telepítsd újra" links to /stacks/<app>/deploy)

Added (backups_restore.html, the precondition notice — renders only when no drive is registered):

Előbb csatold vissza az adatmeghajtót. A mentéseid megvannak, és a meghajtók is megvannak — újratelepítés után viszont a gép még nem ismeri őket, ezért most nincs hová visszaállítani. Ez két kattintás: Tárhely → Meghajtók, „Meglévő meghajtó csatolása". Utána gyere vissza ide."

Changed (offbox_restore.go, the drive refusal):

was: „nincs elérhető adatmeghajtó a visszaállításhoz" now: „nincs regisztrált adatmeghajtó, ezért nincs hová visszaállítani — a meghajtók megvannak, csak újra kell csatolni őket a Tárhely → Meghajtók oldalon, utána ez a visszaállítás működni fog"

Changed (offbox_reconstitute.go, the not-installed refusal):

was: „a(z) %s nincs telepítve — előbb állítsd helyre az alkalmazást, utána az adatokat" now: „a(z) %s nincs telepítve, ezért nincs hová visszaállítani az adatait — telepítsd újra az alkalmazást (Alkalmazások), utána ez a visszaállítás működni fog"

Added (settings_security.html, reveal failure): „A visszaállítási jelszó lekérése nem sikerült." Added (reveal endpoint, nothing stored): „Ezen a gépen nincs tárolt visszaállítási jelszó."


5. Tests and red-proofs — each mutation asserted to have applied

Test Scenario Result Red-proof (mutation → observed)
TestSecurityPage_DoesNotContainTheRetrievalPassphrase A PASS value re-rendered into the page → FAIL, printing the exposure
TestSecurityPage_NoCardWhenNoPassphraseStored A PASS — (gate is on existence, not the value)
TestRevealEndpoint_ReturnsThePassphraseToAnAuthenticatedCaller B PASS remove the route/handler → customer cannot obtain it at all
TestRevealEndpoint_404sWhenNothingStored B PASS
TestRestorePage_NoRegisteredDrive_NamesTheReasonAndTheRoute C PASS precondition block deleted → FAIL
TestRestorePage_HealthyBox_HasNoPreconditionNotice E PASS notice made unconditional ({{if true}}) → FAIL
TestRestorePage_NotInstalled_DoesNotPromiseAReinstall D PASS old promise restored → FAIL on all three assertions
TestHasRestoreDestination_AgreesWithTheResolver seam PASS predicate forced trueFAIL

Red-proof D caught a fault in my own work. On its first run only two of its three assertions failed, because the explanatory HTML comment I had written quoted the old sentence — and HTML comments ship in the response body. The contradiction was still on the page, and the assertion that forbids it could never fail. The comment was rewritten to describe the old string without quoting it; re-run, all three assertions fail under the mutation. This is the same class as the defect being fixed, and it was found only because the red-proof was actually run.

One pre-existing test was updated, not deleted: TestOffsiteRestoreList_RenderedForRebuiltBox pinned the old sentence. Its intent — state the not-installed consequence, do not hide it — is unchanged; it now asserts the consequence and the route.

Green gate: go build ./... && go vet ./... && go test ./...rc 0. controller_gates.py: all nine OK.


6. §7.5 — the currency gate went red, as predicted

newest released controller : 0.207.0
newest golden baked        : 0.206.0
GOLDEN CURRENCY GATE FAILED

The felhom-controller push needed no bypass — that repo's own gates passed. The felhom.eu push used git push --no-verify, declared in that commit message, in this report, and in R-242's row.

A bypass and NOT a waiver, deliberately. The gate offers a waiver for a release that deliberately needs no golden; this release needs one. Recording a waiver would have made the register say something untrue. Owed: bake golden 0.207.0 and vouch it.


7. Part 3 — the record

R-201's state field read PASSED + PROVEN-LIVE 2026-08-04 — the data half, from the drill — while the row body carried the fifth walk's journey pass. Corrected to BOTH HALVES PASSED — DATA 2026-08-04, JOURNEY 2026-08-07, with what it does not claim stated in the field itself: the journey is not smooth, and the discriminator's positive half is unproven.

The census asked for: a sweep of all 146 register rows found NO OTHER state field disagreeing with its own body. Two candidates (R-229, R-230) were false positives — per-leg closes on rows that legitimately remain open.

So the pattern is real but not currently widespread: R-218 on 2026-08-06, R-201 now, nothing else outstanding. The mechanism is worth naming, because it will recur: a row states status in two places — a bold phrase early in the What column and the State cell at the end — and a session that closes an item updates the body and the end cell, while the early phrase, which is what a reader sees first, goes stale.


8. Live validation — what was proven on a box, and what was not

# Check Result
1 curl the security page, passphrase absent PASS, live, with before/after (§1)
2 the reveal works for a customer who asks PASS, live — 200, no-store, correct value, logged
3 the two refusals rendered on VM 325 NOT PROVEN LIVE — stated plainly (below)
4 a healthy box's restore page unchanged PASS, live — no notice, no not-installed hint

Check 3 is the honest gap. R-252's state is rebuild-only: the customer API refuses to deregister the last usable drive („ez az egyetlen használható tárhely — a leszerelés megtagadva"), and when storage_paths was emptied directly the controller re-registered one on restart, so HasRestoreDestination() was correctly true and the notice correctly did not render — which is Scenario E passing live, not a failure. R-253 likewise: after removing the app the controller still reports deployed=true (state stopped), so the row's Installed stays true.

Both notices are therefore pinned at the template + predicate level — by tests that drive the real template and the real predicate, each with a demonstrated red-proof — and not by a live render. I would rather say that than dress the template test up as a live one.


9. Files, commits, deployed version

felhom-controller8dbbc98 (v0.207.0): internal/web/handlers.go · internal/web/server.go · internal/web/templates/settings_security.html · internal/web/templates/backups_restore.html · internal/backup/offbox_restore.go · internal/backup/offbox_reconstitute.go · internal/web/offsite_restore_list_test.go · new: internal/web/retrieval_password_exposure_test.go, internal/web/restore_preconditions_test.go, internal/backup/restore_destination_test.go · CHANGELOG.md · CONTEXT.md · REUSE.md · controller/README.md

felhom.eu1fc3876 (pushed --no-verify, declared): documentation/backlog/OPEN-ITEMS.md (R-249/R-252/R-253 closed, R-254 filed, R-201's state field corrected, R-242 updated) · STATUS.md (93 lines).

Deployed: gitea.dooplex.hu/admin/felhom-controller:0.207.0 — healthy on demo-hp guest 9201 and on VM 325.

Register: highest ID moved R-253 → R-254.


10. What remains open

  • R-254 — the two remaining render-then-hide instances. The recommended next item.
  • R-242 — the bake + vouch of golden 0.207.0 this release now needs; and R-242's own half, that nothing gates the vouch.
  • The discriminator's positive half — the fifth walk exercised R-241's mint guard positively and the fingerprint comparison only negatively. Proving the positive half needs a deliberate fixture (a box holding a divergent key), not a walk — and v0.206.0's guard now prevents that state arising by itself, which is exactly why it needs constructing.
  • Untouched by design: R-250, R-251, R-243, R-244, R-240, R-213, R-202, R-214, R-247, R-248.

11. Observations — noticed, not acted on

  • shares_restore.go:62,81 carries the same „nincs elérhető adatmeghajtó" string with no route, for the SMB-shares restore path. Out of scope (R-252 is the app path); the same one-line treatment would fix it.
  • The reveal endpoint has no rate limit. It is behind session auth + CSRF, so it is not a brute-force surface, but the escrow start handler deliberately rides the login limiter and this does not. Deliberate omission, flagged.
  • The Hetzner token in ~/.config/credentials cannot see storage box 611421GET /v1/storage_boxes/611421/subaccounts → 404 and GET /v1/storage_boxes → 200 with 0 entries. It is scoped to a different project than the hub's. Recorded so the next teardown does not re-derive it.