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.
This commit is contained in:
Audiobuzz
2026-07-04 08:58:38 +00:00
parent 2ef0c1747d
commit ea29d0d12e
+40 -7
View File
@@ -18,6 +18,7 @@ import (
"net/http" "net/http"
"os" "os"
"strconv" "strconv"
"strings"
"sync" "sync"
"time" "time"
@@ -132,15 +133,24 @@ func main() {
s := &server{cfg: cfg, store: store} s := &server{cfg: cfg, store: store}
// Authorise pillar: the enrol-broker admin client (nil = disabled, so the // Authorise pillar: the enrol-broker admin client. Enabled when the broker URL
// manager runs standalone before a broker is wired). // 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) broker, err := newBrokerClient(cfg)
if err != nil { if err != nil {
log.Fatalf("broker client: %v", err) log.Fatalf("broker client: %v", err)
} }
s.broker = broker s.broker = broker
if broker == nil {
log.Printf("authorise pillar DISABLED (set FLEET_MGR_BROKER_URL + client cert/key + CA to enable)")
} }
// Metrics gateway: the manager is the only reader/writer of InfluxDB. nil (a // 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 // 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 // live-stream registry the lifecycle API pushes desired-state through, and it
// writes each snapshot to the metrics sink. // 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) s.hub = newAgentHub(store, sink)
go func() { go func() {
if err := serveAgent(cfg, s.hub); err != nil { if err := serveAgent(cfg, s.hub); err != nil {
log.Fatalf("agent plane: %v", err) 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, // Admin browser auth = WebAuthn/passkey (secure mode only). Sets up the RP,
@@ -292,6 +309,22 @@ func env(k, def string) string {
return def 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 { func envInt(k string, def int) int {
if v := os.Getenv(k); v != "" { if v := os.Getenv(k); v != "" {
if n, err := strconv.Atoi(v); err == nil { if n, err := strconv.Atoi(v); err == nil {