hub v0.66.0 + ISO v1.20.0: customer self-bind (R-27 slice 1)
Let a customer bind their own freshly-installed appliance without the operator: operator "Send self-bind link" mints a 7-day tokenized capability link, emailed (Hungarian, sibling sender) to the customer, who opens a public /bind/<token> page and proves two factors — the console pairing code shown on the box screen + their retrieval passphrase — and the hub stages the bind via the same BindAppliance (provenance customer_selfbind). The box's ~30s appliance poll delivers. Viktor's three rulings verbatim: console pairing code (no appliance list ever rendered), operator-sent tokenized link, 5-attempt lockout -> "call support". Wrong code == wrong passphrase (one generic failure, no oracle, both factors compared unconditionally); expiry falls back to operator-bind unchanged. THE TRAP: one public prefix /bind/, exempt from auth+CSRF at both /login gate sites via a single isPublicBindPath predicate (tight trailing-slash match; ServeMux ..-cleans; handler rejects '/' in token). 9 tests (Scenarios A-F + F1/F2); 4 red-proofs verified red-then-green (lockout, oracle, widened-prefix, single-active). GC verdict: no appliance GC -> the 7-day TTL stands alone. Controller/agent untouched; R-27b deferred. Green: full hub build/vet/test (17 ok) + bash -n + hub confirm gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017qDiBqKKQ5vPB5fXBqu7Kp
This commit is contained in:
@@ -140,7 +140,15 @@ func (h *Handler) handleApplianceRegister(w http.ResponseWriter, r *http.Request
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
isNew, err := h.store.RegisterAppliance(uuid, macSet, sshKeys, hwSummary, sha256hex(token))
|
||||
// v0.66.0 (R-27): a stable 6-char pairing code the box prints on its console + the customer types
|
||||
// into the self-bind page. The candidate is used only on first insert; a re-register keeps the code.
|
||||
candidateCode, err := configgen.RandomPairingCode()
|
||||
if err != nil {
|
||||
h.logger.Printf("[ERROR] appliance register: pairing-code mint: %v", err)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
isNew, pairingCode, err := h.store.RegisterAppliance(uuid, macSet, sshKeys, hwSummary, sha256hex(token), candidateCode)
|
||||
if err != nil {
|
||||
h.logger.Printf("[ERROR] appliance register (uuid=%s): %v", uuid, err)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
@@ -153,7 +161,10 @@ func (h *Handler) handleApplianceRegister(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]any{"appliance_token": token, "poll_interval_sec": 30})
|
||||
// pairing_code is additive — a pre-v0.66.0 bootstrap ignores it and stays operator-bind-only.
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"appliance_token": token, "poll_interval_sec": 30, "pairing_code": configgen.FormatPairingCode(pairingCode),
|
||||
})
|
||||
}
|
||||
|
||||
// handleAppliancePoll — GET /api/v1/appliance/poll (Bearer appliance-token). One-shot delivery:
|
||||
|
||||
Reference in New Issue
Block a user