hub v0.161.0: passphrase keys only until the box uses its own (box-config route; R-1159, D19); ep0 namespaces without a prune job alarmed, tenantsync v1.3.0 creates the job (R-1165)
gates / gates (push) Successful in 7m47s

This commit is contained in:
2026-10-19 15:55:45 +02:00
parent 224fe41e06
commit b6a911c3f9
16 changed files with 802 additions and 35 deletions
+19 -28
View File
@@ -408,6 +408,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case r.Method == http.MethodGet && strings.HasPrefix(path, "/config/"):
customerID := strings.TrimPrefix(path, "/config/")
h.handleConfigRetrieve(w, r, customerID)
case r.Method == http.MethodGet && strings.HasPrefix(path, "/box-config/"):
h.handleBoxConfig(w, r, strings.TrimPrefix(path, "/box-config/"))
case r.Method == http.MethodPost && strings.HasPrefix(path, "/offsite/consume-password/"):
customerID := strings.TrimPrefix(path, "/offsite/consume-password/")
h.handleOffsiteConsumePassword(w, r, customerID)
@@ -1239,6 +1241,11 @@ func (h *Handler) handleHostEnroll(w http.ResponseWriter, r *http.Request) {
return
}
h.noteReenrollOverExistingHost(cc, existing, time.Now())
// R-1159: once the box uses its own key, the passphrase no longer re-serves the host key.
if closed, why := h.passphraseKeysClosed(req.CustomerID); closed {
h.refusePassphraseKeys(w, r, req.CustomerID, "POST /host-enroll", why)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"host_id": existing.HostID, "api_key": existing.APIKey})
@@ -2824,6 +2831,10 @@ func (h *Handler) handleRecovery(w http.ResponseWriter, r *http.Request, custome
http.Error(w, "Unauthorized: invalid password", http.StatusUnauthorized)
return
}
if closed, why := h.passphraseKeysClosed(customerID); closed {
h.refusePassphraseKeys(w, r, customerID, "GET /recovery", why)
return
}
// Generate controller.yaml. The claim state is baked read-only (no issue on the DR path — a
// recovered box whose settings.json is gone re-gates on the EXISTING hash; the reset flow
@@ -2897,36 +2908,16 @@ func (h *Handler) handleConfigRetrieve(w http.ResponseWriter, r *http.Request, c
return
}
if h.templateProvider == nil {
http.Error(w, "Config generation not available", http.StatusServiceUnavailable)
// R-1159: the passphrase gives keys only until the box uses its own (passphrase_scope.go).
if closed, why := h.passphraseKeysClosed(customerID); closed {
h.refusePassphraseKeys(w, r, customerID, "GET /config", why)
return
}
// Customer-claim arc (v0.50.0): the REAL config pull (Day-0 installer / controller refresh) is
// the Day-0 claim entry point — issue + email the first code here (idempotent), and bake the
// active hash into the generated web.claim_code_hash so the box is gated from FIRST boot.
// (The operator-UI preview deliberately does NOT issue — it only bakes an existing hash.)
var claimState *store.ClaimState
if h.claimEngine != nil {
var cerr error
claimState, cerr = h.claimEngine.EnsureIssued(cfg)
if cerr != nil {
// Loud but non-fatal: the config is still served; if a hash was stored the gate is armed
// and the operator resends the email from the customer page.
h.logger.Printf("[WARN] claim issue for %s on config retrieve: %v", customerID, cerr)
}
}
yamlOutput, err := configgen.Generate(h.templateProvider.Template(), cfg, claimState)
if err != nil {
h.logger.Printf("[ERROR] Failed to generate config for %s: %v", customerID, err)
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
h.logger.Printf("[INFO] Config downloaded for customer %s", customerID)
w.Header().Set("Content-Type", "text/yaml; charset=utf-8")
w.Write([]byte(yamlOutput))
// Customer-claim arc (v0.50.0): the REAL config pull (Day-0 installer / controller refresh) is the Day-0 claim
// entry point — serveGeneratedConfig issues + emails the first code (idempotent) and bakes the active hash into
// the generated web.claim_code_hash so the box is gated from FIRST boot. (The operator-UI preview deliberately
// does NOT issue — it only bakes an existing hash.)
h.serveGeneratedConfig(w, cfg, "retrieval passphrase")
}
// artifactManifestResponse is the wire shape the host-bootstrap script consumes: the operator-vouched