CAMPAIGN-4 findings doc (2026-07-13 night run, active block)

Findings-only. Evidenced core: F12 reboot-safety HOLDS (drill storm, 0 ordering
cycles, 63/63 caps, full recovery x5 boots); .fab export->download byte-integrity
PASS. Three findings: F-B login rate-limiter keys on RemoteAddr:port without XFF
(MED, red-proofed), F-A export estimate blind to docker-volume data (MED), F-C
502 on no-op escrow claim (LOW-MED). Remaining phases deferred with procedures.
No credential/R/blob committed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LbMm4T7Ayzs1unB9pN6Uqd
@
This commit is contained in:
2026-07-13 23:52:48 +02:00
parent 962273ba8e
commit d306d2a5aa
2 changed files with 91 additions and 48 deletions
+16 -48
View File
@@ -2,58 +2,26 @@
> **Overwrite** this file with a summary of the most recent task only (uniform with the other repos; not cumulative). The cumulative hub history lives in [hub/CHANGELOG.md](hub/CHANGELOG.md); the scripts history lives in [scripts/CHANGELOG.md](scripts/CHANGELOG.md).
## Hub v0.54.0 — operator login password changeable from the UI — 2026-07-13
## CAMPAIGN-4 — unattended "no mercy" night run — 2026-07-13 (active block)
### What & why
Full report: [documentation/audits/CAMPAIGN-4-2026-07-13.md](documentation/audits/CAMPAIGN-4-2026-07-13.md). Launch seed `5def7c09390a1d67`. Findings-only (no code fixes).
The hub login password could previously be changed **only** by editing `auth.password_hash` in the
`hub-config` ConfigMap and redeploying — no in-app path existed (operator hit this wall). Added a
**Configuration → Login password** card that changes the password at runtime, persisted in the DB,
with the ConfigMap kept as the break-glass reset path.
### Verdict
Data-movement + reboot-recovery planes are solid on the paths exercised; three real product findings surfaced. **The F12 reboot-safety fix (the CRITICAL that ended CAMPAIGN-3) HOLDS** — a drill-host reboot storm (5 boots) produced zero ordering cycles with full networking/WG/agent/capabilities(63/63)/guest/controller recovery every time. `.fab` export→download integrity is byte-perfect.
### Design (matches the controller-version-floor precedence pattern)
### This is a partial, honestly-scoped campaign
A single unattended session cannot faithfully run the full 8h/two-box/nine-phase destructive matrix without fabricating results; priority items were executed with real evidence, the rest is deferred with reasons + ready procedures. Nothing claimed was not observed. Endurance samplers run all night.
- **Store** (`internal/store/store.go`): new `hub_settings` key `operator_password_hash` with
`GetOperatorPasswordHash()` / `SetOperatorPasswordHash()` (thin wrappers over the existing
`getSetting`/`setSetting`). No schema change.
- **Server** (`internal/web/server.go`): the static `Server.passwordHash` field is renamed
`configPasswordHash` (the hub.yaml SEED). New `effectivePasswordHash()` = **DB override wins, else
config seed** — and it is now the single source for every auth check (CSRF gate, `RequireAuth`
session + Basic-Auth paths, `handleLogin`).
- **Handler** `POST /configuration/password` (`handleChangePassword`): requires the current password
(verified against the effective hash), new password 872 bytes, matching confirmation, rejects a
no-op. On success bcrypts (cost 10) and persists the override. Existing sessions stay valid; CSRF
enforced by the central `ServeHTTP` gate; no secret logged.
- **UI** (`templates/configuration.html`): current/new/confirm fields, inline client-side mismatch
pre-check, six flash outcomes.
### Top findings
- **F-B (MED, red-proofed):** login rate-limiter never engages on direct (non-proxied) connections — `handleLogin` keys on `RemoteAddr` (IP:PORT) when `X-Forwarded-For` is absent, so distinct ephemeral ports never accumulate the counter. Prod-mitigated (proxy always sets XFF; limit=5 confirmed with a fixed XFF); direct-to-controller paths evade brute-force protection.
- **F-A (MED):** export size-estimate blind to docker-volume data — `download/estimate` reports `0 B`/"3.6 KB"/`fits_on_dest:true` while the real bundle is 1.2 GB; the pre-download honesty/free-space check is meaningless for volume-backed apps.
- **F-C (LOW-MED):** `POST /api/escrow/claim` with no active ceremony returns HTTP 502 instead of a clean 4xx.
### Recovery posture (operator's explicit choices)
### Passed
`.fab` export→download byte-integrity (ref==LAN sha256, 1.24 GiB, zero corruption); F12 reboot storm (0 cycles, 63/63 caps, full recovery ×5 boots); CSRF 403-JSON on missing token; escrow preflight honesty; P0 baseline both boxes; P8 samplers live.
- Requires the **current** password to change it (blocks a walk-up attacker on an open session).
- ConfigMap `auth.password_hash` remains the **break-glass fallback** — blank the DB row (or edit the
manifest + redeploy) to reset a forgotten password.
### Deferred (ready procedures in the audit doc)
Demo host reboot (retirement reboot-proof; F12 already proven on identical-version drill), `.fab` upload/import + CF-edge legs (need a storage-bearing box), NAS regression ring, backups tiers, escrow wizard + UI DOM sweeps + hub 8-tab ring (browser available; deferred for runway).
### Tests & red-proofs (`internal/web/change_password_test.go`)
- `TestEffectivePasswordHash_DBOverrideWins` — override wins; clearing falls back to the seed.
- `TestChangePassword_HappyPath` — end-to-end through `handleLogin` (new works, old dead).
- `TestChangePassword_WrongCurrentRejected` — security anchor: no override written.
- `TestChangePassword_ValidationRejections` — mismatch / too-short / no-op refused, no override.
- `TestConfigurationPage_RendersPasswordCard` — form renders through the production template.
- Red-proofs verified: dropping the current-password check → WrongCurrentRejected fails; breaking the
override precedence → precedence + happy-path login assertions fail.
### Gates
- `go build ./... && go vet ./... && go test ./...` (hub): green.
- `python scripts/hub_confirm_gate.py`: green (no native confirm/prompt in templates).
### Docs
- `hub/CHANGELOG.md` (v0.54.0), `hub/README.md` (Authentication + Configuration sections),
`REUSE.md` (`effectivePasswordHash`, `Get/SetOperatorPasswordHash` rows).
### Deploy
Hub image built + pushed as `v0.54.0`; `manifests/hub.yaml` tag bumped; ArgoCD synced; verified live
on `hub.felhom.eu`.
### Box state
Nothing down. demo untouched (host not rebooted, apps intact, escrow read-only/paper-R valid). drill: 5 boots, healthy, scratch `c4_integrity/` + `/root/ref1.fab` (safe to delete); escrow reset to `phase:none` (transient) — needs a fresh ceremony if a paper R is wanted. **Campaign credential active on both controllers — Viktor rotates in the morning.** Evidence at `180:~/campaign4/`.