hub-safety session: R-135/R-133/R-604/R-530/R-508/R-509/R-880 closed, R-861/R-173/R-518/R-519 narrowed, R-879/R-881 opened (336 → 332); 03 §3.1, 05 §16, golden 0.296.0, the hub-DB off-site plan, STATUS
gates / gates (push) Successful in 32s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-05 13:47:57 +02:00
parent 2b30733b0d
commit 0826e41b31
44 changed files with 1511 additions and 31 deletions
+3 -3
View File
@@ -3,19 +3,19 @@
**Operator action on deploy: none.** Scripts that POST to the hub with the operator password must now send
`X-Felhom-Operator: cli` (the build-deploy skill and the memory note say so).
- **R-135 (`05` §8.1).** A state-changing request without a session cookie used to pass the CSRF gate unconditionally
- **R-135 (`05` §16.1).** A state-changing request without a session cookie used to pass the CSRF gate unconditionally
(measured: a Basic-auth POST with no cookie reached the handler). Now it passes only with Basic credentials AND the
`X-Felhom-Operator` header — a page on another site cannot add a custom header (no CORS preflight is answered), so a
browser with cached Basic credentials can no longer be made to POST. The session path is unchanged (cookie + token).
`web/r135_csrf_test.go` drives all 38 state-changing routes plus an unknown path (39 paths) through RequireAuth → ServeHTTP;
red-proof: the old `return true` lets all 39 through (none answers 403).
- **R-133 (`05` §8.2).** `host_recovery.secret` (each box's break-glass `root@pam` password) is sealed with the SAME
- **R-133 (`05` §16.2).** `host_recovery.secret` (each box's break-glass `root@pam` password) is sealed with the SAME
AES-256-GCM seal and key as the off-site passwords (`OFFSITE_SECRET_KEY`, `store/offsite_seal.go` — reused, not a second
scheme). Existing rows are sealed in place at start-up (`SealLegacyRecoverySecrets`, beside the off-site one). No key →
the save is refused; a wrong key → the reveal is a 500 with nothing in the body or the log, and no "revealed" event.
Both retrieval paths (the operator page and the global-key API) open it through the same store call.
`store/r133_recovery_seal_test.go`, `web/r133_reveal_wrongkey_test.go`, `cmd/hub/r133_wiring_test.go`; 2 red-proofs.
- **R-604 (`05` §7).** A global floor raise now logs one line per customer whose OWN floor is lower (it wins, so the
- **R-604 (`05` §5).** A global floor raise now logs one line per customer whose OWN floor is lower (it wins, so the
raise does not move that box) and sends ONE operator mail naming them (`floor_raise_skipped`, warning, operator-only).
A per-customer floor now records when it was set (`customer_configs.min_controller_set_at`); the System page has a
"Version floors" table: the global floor, every per-customer floor with its age, and which ones the global cannot
+1 -1
View File
@@ -882,7 +882,7 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
// does NOT prove the request is programmatic. A page on another site can make the browser POST a
// form with the operator's cached Basic credentials; it cannot add a custom header (that needs a
// CORS preflight, which the hub never answers). So the header is the proof the old check assumed.
// Decided by CC — operator may reverse (`05` §8.1). Pinned by r135_csrf_test.go.
// Decided by CC — operator may reverse (`05` §16.1). Pinned by r135_csrf_test.go.
const OperatorCLIHeader = "X-Felhom-Operator"
// validateCSRF checks a state-changing request (R-135). Two ways pass, nothing else: