slice 10B: operator-signed destructive completion (offline key + signing CLI) (v0.16.0)
A destructive op runs ONLY on a pinned-key-verified, nonce-fresh, in-window, host-bound, durable-id-bound operator signature. New cmd/felhom-opsign signs canonical OpBlobs offline via ssh-keygen -Y sign (hardware-ready); the signing key is never in the hub or agent. New internal/signedjobs runner verifies each queued blob through the gate and only on all-pass runs the WipeExecutor, which re-resolves the DURABLE device id + re-inspects (8C) before mkfs — closing the 8C data-bearing-wipe pending_signature gap. New storage durable-device resolution; authz.CanonicalBlob promoted to production. Real-crypto tests assert valid executes and forged/replay/expired/retarget/non-pinned are rejected (executor never called). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,7 @@ import (
|
||||
applog "gitea.dooplex.hu/admin/felhom-agent/internal/log"
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/pbs"
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/provision"
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/signedjobs"
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/proxmox"
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/reconcile"
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/storage"
|
||||
@@ -41,7 +42,7 @@ import (
|
||||
|
||||
// version is the agent version. Overridable at build time with
|
||||
// -ldflags "-X main.version=<v>"; defaults to the in-repo CHANGELOG version.
|
||||
var version = "0.15.0"
|
||||
var version = "0.16.0"
|
||||
|
||||
func main() {
|
||||
var (
|
||||
@@ -245,7 +246,9 @@ func runDaemon(cfg config.Config, logger *slog.Logger) int {
|
||||
desiredProvider := reconcile.NewCachingProvider()
|
||||
// The "Down" channel sync hook: on each heartbeat, fetch desired-state when the generation
|
||||
// advances. The loop calls it via the EnvelopeObserver seam (hub does not import desired).
|
||||
loop.SetEnvelopeObserver(desired.NewSyncer(client, desiredProvider, logger))
|
||||
desiredSyncer := desired.NewSyncer(client, desiredProvider, logger)
|
||||
// The signed-jobs runner (slice 10B) is wired as a SECOND envelope observer below (after the
|
||||
// gate is built) — when the heartbeat flags pending signed ops, it fetches + verifies + executes.
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
@@ -287,6 +290,18 @@ func runDaemon(cfg config.Config, logger *slog.Logger) int {
|
||||
}
|
||||
gate := reconcile.NewGate(verifier, cfg.Hub.HostID, reconcile.SlogAudit{Logger: logger}, logger)
|
||||
|
||||
// Signed-jobs runner (slice 10B): the consumer of the hub's signed-jobs queue. On a heartbeat
|
||||
// that flags pending signed ops, it fetches each opaque blob, runs it through the gate (the
|
||||
// LOCKED authz pipeline: pinned-key SSHSIG → namespace → allow-list → crypto → host → time →
|
||||
// durable nonce-burn) and, only on all-pass, hands the verified op to the storage-WIPE executor
|
||||
// — which re-resolves the DURABLE device id + re-inspects (8C) before mkfs. This closes the 8C
|
||||
// data-bearing `pending_signature` gap. With no signers pinned the gate refuses every job
|
||||
// (pending_signature) and nothing executes — correct. Wired as a second envelope observer
|
||||
// alongside the desired-state syncer.
|
||||
wipeExec := signedjobs.NewWipeExecutor(hostOps, logger)
|
||||
jobsRunner := signedjobs.NewRunner(client, gate, wipeExec, cfg.Hub.HostID, logger)
|
||||
loop.SetEnvelopeObserver(hub.MultiObserver(desiredSyncer, jobsRunner))
|
||||
|
||||
// Storage watchdog (slice 5): the third daemon goroutine. Fast-polls the known target
|
||||
// set for attached↔disconnected transitions → debounced out-of-band report; and, on a
|
||||
// known mount-backed target's device returning unmounted, dispatches a benign re-mount
|
||||
@@ -510,6 +525,7 @@ func buildLocalAPIServer(cfg config.Config, px *proxmox.Client, store *backup.St
|
||||
// Host metrics (slice 9): the shared collector serves GET /host/metrics — a fresh host +
|
||||
// per-storage view to the customer's monitoring page (reuses the slice-4 collector).
|
||||
HostMetrics: collector,
|
||||
HostID: cfg.Hub.HostID, // slice 10B: anti-retarget host in the data-bearing-format pending-op
|
||||
Logger: logger,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user