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:
2026-07-17 15:07:31 +02:00
parent 3172df1927
commit 36c5cd5fdf
16 changed files with 1531 additions and 90 deletions
+13
View File
@@ -316,6 +316,19 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Hosts — read-only fleet view (audit F-M1) + the v0.46.0 log-bundle actions.
case path == "/hosts" || path == "/hosts/":
s.handleHostsList(w, r)
// R-21 slice C — unclaimed-appliance operator actions (bind/discard). POST only.
case strings.HasPrefix(path, "/appliances/") && strings.HasSuffix(path, "/bind"):
if id, ok := parseApplianceID(path, "bind"); ok && r.Method == http.MethodPost {
s.handleApplianceBind(w, r, id)
} else {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
case strings.HasPrefix(path, "/appliances/") && strings.HasSuffix(path, "/discard"):
if id, ok := parseApplianceID(path, "discard"); ok && r.Method == http.MethodPost {
s.handleApplianceDiscard(w, r, id)
} else {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
// v0.47.0 stale host removal — suffix routes BEFORE the bare /hosts/ catch-all
// (mirroring the request-logs placement).
case strings.HasPrefix(path, "/hosts/") && strings.HasSuffix(path, "/delete-impact"):