44ec06b50f
Hetzner storage-box provisioning against api.hetzner.com/v1 (NOT .cloud).
internal/hetznerapi (typed client + CloudAPI interface + Fake + WaitAction);
internal/offsite (Provisioner.ProvisionOffsite — idempotent by label, shared
sub-account/dedicated box, transient password, non-secret Descriptor,
fail-closed); one_time_secrets store (single-use Save/Consume); POST
/offsite/consume-password/{id} (customer-key auth, once); config-form Offsite
section → applyOffsite (502+no-save on error) → descriptor in ConfigJSON →
version bump. Token/passwords never logged/committed/in ConfigJSON. Tested vs a
faked Cloud API + fail-closed red-proof. NOT yet live-provisioned (needs the
dedicated-project scoped token; current token can delete ep0).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
32 lines
1.2 KiB
Go
32 lines
1.2 KiB
Go
package api
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"net/http"
|
|
)
|
|
|
|
// handleOffsiteConsumePassword serves the one-time transient offsite password to the controller EXACTLY
|
|
// ONCE (SLICE 1; SLICE 2 controller consumes it, installs its key, then the hub resets the box password).
|
|
// Auth = the customer's API key (same credential as config-pull); the token's customer must match the
|
|
// path. The value is returned once then marked consumed — a second call 404s. NEVER logged.
|
|
func (h *Handler) handleOffsiteConsumePassword(w http.ResponseWriter, r *http.Request, customerID string) {
|
|
authCustomerID, isGlobal, ok := h.checkAuthCustomer(r)
|
|
if !ok || (!isGlobal && authCustomerID != customerID) {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
pw, err := h.store.ConsumeOneTimeSecret(customerID)
|
|
if err == sql.ErrNoRows {
|
|
http.Error(w, "no unconsumed offsite password", http.StatusNotFound)
|
|
return
|
|
}
|
|
if err != nil {
|
|
h.logger.Printf("[ERROR] offsite consume-password %s: %v", customerID, err) // no secret
|
|
http.Error(w, "internal error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]string{"password": pw}) // one-time; never logged
|
|
}
|