diff --git a/CONTEXT.md b/CONTEXT.md index a5a044b..98a25a5 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -7,6 +7,15 @@ > > Ask Claude Code: "Please update CONTEXT.md with what we did today" +Last updated: 2026-07-07 (v0.103.0 — F-C2-1 config-loader auth-integrity fix) + +> **2026-07-07 — v0.103.0: F-C2-1 (LIVE on 9201).** The config loader ran `os.ExpandEnv` over the +> whole YAML before parse, silently corrupting a bcrypt `web.password_hash` (`$2a$10$…` → `"a0"`) — a +> silent auth-integrity bug. Removed both `ExpandEnv` calls (parse raw bytes); typed +> `FELHOM_WEB_PASSWORD_HASH` override unchanged. Live-proven: a bcrypt hash in controller.yaml now +> loads intact and login succeeds (pre-fix it corrupted → login fail). Behavior change: literal +> `${VAR}` in a value is now preserved verbatim (no repo config depends on the old expansion). + Last updated: 2026-07-06 (v0.102.0 — async restore family; F4 re-adjudicated + fixed) > **2026-07-06 — v0.102.0: async restore family (F4 UX fix, LIVE on 9201).** All three restore surfaces diff --git a/REPORT.md b/REPORT.md index ee2905e..99a9f29 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,43 +1,31 @@ -# REPORT — v0.102.0: async restore family (no proxy-timeout error page on a succeeding restore) +# REPORT — v0.103.0: F-C2-1 config loader no longer corrupts a bcrypt password_hash -**Date:** 2026-07-06 · **Class:** implementation (code + tests + deploy + live acceptance). -**Baseline:** `main` @ `9d5a588` (v0.101.0) → `c529a45`. Part B of the pilot-blockers bundle (A = agent -v0.73.0 F2, C = catalog healthcheck sweep). Provenance: RERUN F4 re-adjudication. +**Date:** 2026-07-07 · **Class:** implementation (controller). **Baseline:** `main` @ `cd0ebd2` +(v0.102.0) → `02d37a1`. Part C of the campaign-2 R1/R2 bundle. -## The bug (F4 re-adjudicated) -All three restore surfaces blocked the HTTP request until completion: `/backup/restore` (→ -`RestoreFromRecoveryUnit`), `/backup/tier2/restore` (→ `RestoreTier2Files`), `/backup/offbox/restore` (→ -`RestoreOffbox`). Through cloudflared's hard 100s cap + traefik a real customer got an **error page while -the restore silently succeeded**; the off-box handler was worse — it bounded on `r.Context()`, so a proxy -read-timeout **canceled the SFTP restore mid-flight**. +## The bug (F-C2-1, silent auth-integrity) +`loadAndParse` (config.go:234) and `LoadFromBytes` (:249) ran `os.ExpandEnv` over the ENTIRE YAML +before parse. A bcrypt hash (`$2a$10$…`) is full of `$word` sequences, so `ExpandEnv` silently +replaced each with its (usually empty) env value — corrupting `web.password_hash` on load. Proven: +`$2a$10$N9qo8uL…` → `"a0"`. Depending on the hash this bricks login or degrades toward a bypass. ## The fix -- **Async family** (mirrors the live `offboxRunHandler`): each handler fast-path refuses a concurrent op - (`IsRunning()` → "Egy mentési/visszaállítási művelet már fut."), runs the restore in a **background - goroutine**, and redirects immediately with "Visszaállítás elindult…". The offbox restore's context moved - from `r.Context()` to `context.Background()+30m` (fixes the mid-flight cancel). The restore functions' - internal single-flight acquire is unchanged. -- **Op-status** (`internal/backup/opstatus.go`): mutex-guarded in-memory current-op + terminal - `last{op,stack,ok,message,finished_at}` (deep-copy getter); new `GET /api/backup/restore-status` (distinct - from `/backup/status`, which proxies the agent PBS status). In-memory (lost on restart). -- **UI** (`backups.html`): a banner polls the status every 3s — neutral running (shown even on a fresh page - load mid-op), success on completion, red only on failure. +Removed both `os.ExpandEnv` calls — parse the raw bytes directly. The sanctioned typed env path +(`applyEnvOverrides` → `FELHOM_WEB_PASSWORD_HASH`, applied after parse) is unchanged; no shipped +`controller.yaml` relies on file-level `${VAR}` interpolation (only `docker-compose.yml`'s `${DOMAIN}`, +which is compose-level). Behavior change: a literal `${VAR}` in a value is now preserved verbatim. ## Tests + red-proof -`opstatus_test.go` (begin→running→terminal, deep-copy, failure); `async_restore_test.go` (handler returns -<500ms while the restore parks in a blocking provider + op-status transitions; double-click refused with no -second launch). Red-proof: reverting the handler to synchronous blocks the request indefinitely (killed at -30s) vs <500ms async. Full gate `go build/vet/test ./...` = PASS. +`config_test.go`: bcrypt hash loads byte-identical (file + bytes paths) — **red-proof:** pre-fix +`ExpandEnv` mangles it to `"a0"` → FAIL, demonstrated + reverted; `FELHOM_WEB_PASSWORD_HASH` override +still wins; literal `${VAR}` preserved. Full gate `go build/vet/test ./...` = PASS. ## Deploy + live acceptance -Built + pushed `:0.102.0` on 180, deployed to guest 9201 (golden/bootstrap), healthy; `GET -/api/backup/restore-status` live. **Restore drill (vaultwarden):** planted canary → backup → vandalized → -`POST /backup/restore` returned in **0.018s** (302, "elindult" flash); status transitioned -`running:true → false` with terminal `{op:restore, ok:true}`; **canary restored bit-identical**. **External -path (F4):** `POST https://felhom.demo-felhom.eu/backup/restore` (through cloudflared) returned in **0.235s** -with 302 — no proxy error. **Tier-2:** `POST /backup/tier2/restore` async 0.007s; its failure landed in the -terminal status (B3 failure-visibility). Double-click refusal covered by the unit test. +Built + pushed `:0.103.0`, deployed to guest 9201 (golden/bootstrap), healthy. **F-C2-1 live belt:** +put a real bcrypt hash IN controller.yaml (the exact bug path), restarted → login with the correct +password **succeeded** (302, no "Hibás jelszó") — the hash loaded intact; pre-fix it would corrupt → +fail. Reverted controller.yaml to the auth-off pre-state (root page 200). -## Not changed -The restore functions' internal single-flight; `/api/backup/status` (agent-PBS) semantics; the restore-button -CSRF/form mechanism (only the handler tail went async); no op-status persistence (documented). +## Observation +Migration note: any hand-edited `controller.yaml` carrying a `${...}` that expected file-level +expansion would change behavior — none exist in the repo; the golden/bootstrap writes concrete values.