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
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:
+3
-3
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user