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:
@@ -8,6 +8,7 @@
|
||||
</head>
|
||||
<body>
|
||||
{{template "icon_sprite"}}
|
||||
{{template "inline_confirm_js"}}
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>Felhom <span>Hub</span></h1>
|
||||
@@ -23,6 +24,58 @@
|
||||
|
||||
<h2 style="margin-bottom: 1rem;">Hosts</h2>
|
||||
|
||||
{{if .Flash}}
|
||||
<div class="flash flash-success" style="margin-bottom: 1rem;">
|
||||
{{if eq .Flash "appliance_bound"}}Appliance bound — its credentials are delivered on its next poll (within ~30s); it then completes day-0 install.
|
||||
{{else if eq .Flash "appliance_discarded"}}Appliance discarded — its token is invalidated; further polls are ignored.
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .Unclaimed}}
|
||||
<section class="card" style="margin-bottom: 1.5rem; border-color: var(--warn);">
|
||||
<h2 style="margin-top: 0;">Unclaimed appliances <span class="text-muted" style="font-size: 0.8em; font-weight: normal;">(booted from the generic ISO, awaiting a bind)</span></h2>
|
||||
<p class="text-muted" style="margin-top: 0;">A box that installed from the universal secret-free ISO and registered itself. <strong>Bind</strong> it to a customer to deliver its retrieval passphrase once; <strong>discard</strong> to ignore it.</p>
|
||||
<div style="overflow-x: auto;">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr><th>Appliance</th><th>MACs</th><th>Hardware</th><th>SSH host keys</th><th>Seen</th><th>Bind to customer</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Unclaimed}}
|
||||
<tr>
|
||||
<td><code style="font-size: 0.8em;">{{.UUID}}</code>
|
||||
{{if .Stale}}<br><span class="status-badge status-warn" title="No poll in over 7 days">stale</span>{{end}}
|
||||
{{if .Bound}}<br><span class="status-badge status-ok" title="Bound — awaiting the box's next poll">bound → {{.BoundCustomer}}</span>{{end}}
|
||||
</td>
|
||||
<td style="font-size: 0.78em; font-family: var(--font-mono)">{{range .MACs}}{{.}}<br>{{end}}</td>
|
||||
<td style="font-size: 0.8em;">{{if .Product}}{{.Product}}<br>{{end}}{{if .CPU}}<span class="text-muted">{{.CPU}}</span><br>{{end}}{{if .MemGB}}<span class="text-muted">{{.MemGB}}</span>{{end}}</td>
|
||||
<td style="font-size: 0.72em; font-family: var(--font-mono)">{{range .SSHFingerprints}}{{.}}<br>{{end}}</td>
|
||||
<td style="font-size: 0.78em;">{{if .FirstSeen}}first {{timeAgoPtr .FirstSeen}}<br>{{end}}{{if .LastSeen}}last {{timeAgoPtr .LastSeen}}{{end}}</td>
|
||||
<td>
|
||||
<form method="POST" action="/appliances/{{.ID}}/bind" style="display: flex; flex-direction: column; gap: 0.3rem;">
|
||||
<input type="hidden" name="_csrf" value="{{$.CSRFToken}}">
|
||||
<select name="customer_id" required style="max-width: 16em;">
|
||||
<option value="">— pick a customer —</option>
|
||||
{{range $.CustomerPicker}}<option value="{{.CustomerID}}">{{.CustomerName}} ({{.HostCount}} host{{if ne .HostCount 1}}s{{end}})</option>{{end}}
|
||||
</select>
|
||||
<button type="submit" class="btn btn-sm" style="border-color: var(--warn); color: var(--warn);">Bind & deliver</button>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<form method="POST" action="/appliances/{{.ID}}/discard">
|
||||
<input type="hidden" name="_csrf" value="{{$.CSRFToken}}">
|
||||
<button type="submit" class="btn btn-sm btn-outline" data-confirm="Discard this appliance? Its token is invalidated and further polls are ignored.">Discard</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
{{if .Hosts}}
|
||||
<section class="card" style="padding: 0; overflow: hidden;">
|
||||
<table class="data-table">
|
||||
|
||||
Reference in New Issue
Block a user