hub v0.62.0 + scripts v1.19.0 — R-21 slice C: the universal secret-free ISO
A generic ISO carries NO customer secret. The box registers itself at the hub as an unclaimed appliance; the operator binds it to a customer; the hub delivers the customer-id + retrieval passphrase ONCE; day-0 completes via the slice-A path. Hub (v0.62.0): - store/appliance.go: appliance_registrations keyed by (uuid, mac_set) — MAC set is the tiebreaker (duplicate SMBIOS UUIDs); token stored as sha256 only. Idempotent register (sticky-discard), atomic one-shot delivery, bind/discard. - api/appliance.go: POST /appliance/register (the one unauth endpoint, per-IP rate-limited, 256-bit token); GET /appliance/poll (404 no-oracle / 204 unbound / 200 deliver-once / 410 delivered). Passphrase read live, never logged. - web/appliances.go: Hosts-page "Unclaimed appliances" section + BIND (customer picker, host count display-only) + DISCARD; SSH host-key fingerprints; events. - Red-proofs: one-shot delivery + register idempotency (both proven red); 404-no-oracle, sticky-discard, bind staging, render. Green + confirm gate. Scripts (v1.19.0): - felhom-bootstrap.sh: ONE unit, TWO modes. Direct (env has customer/passphrase) = slice-A path, byte-identical, only branched around. Pairing (generic) = register + poll (RestartSec=30 is the poll timer); on delivery write the env 0600 and fall through to direct. Secrets + token shredded on success. - build-felhom-iso.sh --pairing: generic secret-free ISO, -generic filename, manifest mode=pairing. profiles/generic.profile (new). - test/bootstrap-modes.sh: Scenario D (direct = zero appliance calls) + pairing register/poll + delivery handoff — all green in a debian container.
This commit is contained in:
@@ -53,6 +53,11 @@ type Handler struct {
|
||||
mailLimiter *mailRateLimiter
|
||||
mailFromAllow map[string]bool
|
||||
|
||||
// applianceLimiter (v0.62.0, R-21 slice C) throttles the ONE unauthenticated endpoint,
|
||||
// POST /api/v1/appliance/register, per client IP — the unclaimed population is tiny and the
|
||||
// ingress already geo-restricts to HU, so this is a cheap anti-abuse bound, not a fleet lever.
|
||||
applianceLimiter *ipRateLimiter
|
||||
|
||||
// S1 offsite connectivity: the wgsync reconciler seam (internal/api/wg.go). nil = peer-sync
|
||||
// disabled — mutations still persist, responses carry sync:"disabled".
|
||||
wgSyncer WGSyncer
|
||||
@@ -117,6 +122,7 @@ func New(store *store.Store, apiKey, resendAPIKey, fromEmail string, templatePro
|
||||
logger: logger,
|
||||
httpClient: &http.Client{Timeout: 10 * time.Second},
|
||||
templateProvider: templateProvider,
|
||||
applianceLimiter: newIPRateLimiter(20), // 20 registrations/min/IP burst — booting boxes retry ~30s
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,6 +202,12 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// operator-intent bump for the box's customer, then the box fires its ordinary report.
|
||||
case r.Method == http.MethodGet && path == "/wait":
|
||||
h.handleWait(w, r)
|
||||
// R-21 slice C — the universal ISO. register is the ONE unauthenticated endpoint (per-IP
|
||||
// rate-limited); poll is Bearer appliance-token. Both minimal, no enumeration oracle.
|
||||
case r.Method == http.MethodPost && path == "/appliance/register":
|
||||
h.handleApplianceRegister(w, r)
|
||||
case r.Method == http.MethodGet && path == "/appliance/poll":
|
||||
h.handleAppliancePoll(w, r)
|
||||
case r.Method == http.MethodPost && path == "/host-report":
|
||||
h.handleHostReport(w, r)
|
||||
case r.Method == http.MethodPost && path == "/host-enroll":
|
||||
|
||||
Reference in New Issue
Block a user