From ea29d0d12e9f2d4e0adf6b43d79e919ab2fca121 Mon Sep 17 00:00:00 2001 From: Audiobuzz Date: Sat, 4 Jul 2026 08:58:38 +0000 Subject: [PATCH] fleet-manager: soft-disable optional planes when their PKI isn't staged yet The compose wires the authorise (broker) and agent planes to fixed cert paths, so a first bring-up that hasn't run the PKI steps yet crash-looped on the first missing file (e.g. /pki/broker/clients/fleet-manager.crt) instead of booting. Treat a configured-but-absent cert as "plane not yet provisioned": log a loud WARNING and disable that plane, so the manager boots and each plane lights up as its PKI appears. A malformed/unreadable cert still hard-fails (real misconfig). The admin plane stays a hard requirement (FLEET_MGR_ADMIN_INSECURE is its bench escape). Adds a missingFiles() helper; hub push path already nil-guards a disabled agent plane. --- serving/fleet-manager/main.go | 55 ++++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/serving/fleet-manager/main.go b/serving/fleet-manager/main.go index 4d2f21a..dcacade 100644 --- a/serving/fleet-manager/main.go +++ b/serving/fleet-manager/main.go @@ -18,6 +18,7 @@ import ( "net/http" "os" "strconv" + "strings" "sync" "time" @@ -132,15 +133,24 @@ func main() { s := &server{cfg: cfg, store: store} - // Authorise pillar: the enrol-broker admin client (nil = disabled, so the - // manager runs standalone before a broker is wired). - broker, err := newBrokerClient(cfg) - if err != nil { - log.Fatalf("broker client: %v", err) - } - s.broker = broker - if broker == nil { + // Authorise pillar: the enrol-broker admin client. Enabled when the broker URL + // is set AND its client PKI is present. Missing PKI (not yet provisioned) + // DISABLES the pillar with a loud warning rather than crash-looping the whole + // manager — so a partial bring-up boots and the pillar lights up once the cert + // is staged. A malformed/unreadable cert still hard-fails (a real misconfig). + switch { + case cfg.brokerAdminURL == "": log.Printf("authorise pillar DISABLED (set FLEET_MGR_BROKER_URL + client cert/key + CA to enable)") + default: + if miss := missingFiles(cfg.brokerClientCert, cfg.brokerClientKey, cfg.brokerCA); len(miss) > 0 { + log.Printf("WARNING: authorise pillar DISABLED — broker PKI not found: %s (run enrol-broker/admin-pki.sh; see DEPLOY.md)", strings.Join(miss, ", ")) + break + } + broker, err := newBrokerClient(cfg) + if err != nil { + log.Fatalf("broker client: %v", err) + } + s.broker = broker } // Metrics gateway: the manager is the only reader/writer of InfluxDB. nil (a @@ -159,15 +169,22 @@ func main() { // plane can run standalone (e.g. before any host is enrolled). The hub is the // live-stream registry the lifecycle API pushes desired-state through, and it // writes each snapshot to the metrics sink. - if cfg.agentCert != "" && cfg.agentKey != "" && cfg.agentClientCA != "" { + switch { + case cfg.agentCert == "" || cfg.agentKey == "" || cfg.agentClientCA == "": + log.Printf("agent plane DISABLED (set FLEET_MGR_AGENT_CERT/_KEY/_CLIENT_CA to enable)") + default: + if miss := missingFiles(cfg.agentCert, cfg.agentKey, cfg.agentClientCA); len(miss) > 0 { + // Same soft-disable as the authorise pillar: boot without the agent + // plane until its fleet-CA PKI is staged, rather than crash-looping. + log.Printf("WARNING: agent plane DISABLED — PKI not found: %s (run server-pki.sh; see DEPLOY.md)", strings.Join(miss, ", ")) + break + } s.hub = newAgentHub(store, sink) go func() { if err := serveAgent(cfg, s.hub); err != nil { log.Fatalf("agent plane: %v", err) } }() - } else { - log.Printf("agent plane DISABLED (set FLEET_MGR_AGENT_CERT/_KEY/_CLIENT_CA to enable)") } // Admin browser auth = WebAuthn/passkey (secure mode only). Sets up the RP, @@ -292,6 +309,22 @@ func env(k, def string) string { return def } +// missingFiles returns the subset of the given paths that don't exist (or aren't +// stat-able) on disk. Used to soft-disable an optional plane whose PKI hasn't +// been provisioned yet, instead of crash-looping the whole manager. +func missingFiles(paths ...string) []string { + var miss []string + for _, p := range paths { + if p == "" { + continue + } + if _, err := os.Stat(p); err != nil { + miss = append(miss, p) + } + } + return miss +} + func envInt(k string, def int) int { if v := os.Getenv(k); v != "" { if n, err := strconv.Atoi(v); err == nil {