// fleet-manager — server-side control plane for the diskless kiosk fleet. // // Two planes, mirroring the enrol-broker's two-listener pattern but with distinct // trust anchors (see docs/tpm-enrolment-design.md §5 "identity"): // // - ADMIN plane (this checkpoint): HTTPS + mTLS verified by the DEDICATED admin // CA (admin-pki.sh) — NOT the fleet CA — so an enrolled host cert can never // reach approve/deny/lifecycle. Serves the JSON API and (later) the SPA to // admin browsers. // - AGENT plane (checkpoint 2): gRPC + mTLS verified by the FLEET CA — the // host's reused TPM identity cert. Bidi stream: health/metrics up, // desired-state down. // // Checkpoint 1 wires only the admin plane + inventory + a seed host. package main import ( "crypto/tls" "crypto/x509" "log" "net/http" "os" "strconv" "time" ) type config struct { adminAddr string adminInsecure bool adminCert string adminKey string adminClientCA string // dedicated admin CA (verifies admin browser certs) inventoryPath string seed bool hostnqnBase string } func loadConfig() config { return config{ adminAddr: env("FLEET_MGR_ADMIN_ADDR", "127.0.0.1:9500"), adminInsecure: env("FLEET_MGR_ADMIN_INSECURE", "") == "true", adminCert: env("FLEET_MGR_ADMIN_CERT", ""), adminKey: env("FLEET_MGR_ADMIN_KEY", ""), adminClientCA: env("FLEET_MGR_ADMIN_CLIENT_CA", ""), inventoryPath: env("FLEET_MGR_INVENTORY", "/var/lib/fleet-manager/inventory.json"), seed: env("FLEET_MGR_SEED", "") == "true", hostnqnBase: env("HOSTNQN_BASE", "nqn.2026-06.org.example.fleet:host-"), } } // server bundles the shared dependencies the handlers close over. type server struct { cfg config store Store } func main() { log.SetFlags(log.LstdFlags | log.LUTC) cfg := loadConfig() store, err := newJSONStore(cfg.inventoryPath) if err != nil { log.Fatalf("inventory: %v", err) } if cfg.seed { if n := seedHosts(store, cfg.hostnqnBase); n > 0 { log.Printf("seeded %d demo host(s) (FLEET_MGR_SEED=true — dev only)", n) } } s := &server{cfg: cfg, store: store} mux := http.NewServeMux() s.routes(mux) srv := &http.Server{ Addr: cfg.adminAddr, Handler: mux, ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 15 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 60 * time.Second, } if cfg.adminInsecure { log.Printf("WARNING: admin plane INSECURE (plaintext, no client-cert auth) on %s — dev/lab only", cfg.adminAddr) log.Printf("fleet-manager admin (insecure) listening on %s", cfg.adminAddr) log.Fatal(srv.ListenAndServe()) } tlsCfg, err := adminTLS(cfg) if err != nil { log.Fatalf("admin TLS: %v", err) } srv.TLSConfig = tlsCfg log.Printf("fleet-manager admin (mTLS) listening on %s", cfg.adminAddr) log.Fatal(srv.ListenAndServeTLS("", "")) // certs are in TLSConfig } // adminTLS builds the mTLS server config: our server cert + REQUIRE a client cert // signed by the dedicated admin CA. Reusing the fleet CA here would let any host // cert authenticate to the admin plane — privilege escalation — so it is a hard // error to omit the admin client CA in secure mode. func adminTLS(cfg config) (*tls.Config, error) { if cfg.adminCert == "" || cfg.adminKey == "" { return nil, errString("FLEET_MGR_ADMIN_CERT and FLEET_MGR_ADMIN_KEY are required (or set FLEET_MGR_ADMIN_INSECURE=true for dev)") } if cfg.adminClientCA == "" { return nil, errString("FLEET_MGR_ADMIN_CLIENT_CA is required — the dedicated admin CA that verifies admin browser certs (must NOT be the fleet CA)") } cert, err := tls.LoadX509KeyPair(cfg.adminCert, cfg.adminKey) if err != nil { return nil, err } caPEM, err := os.ReadFile(cfg.adminClientCA) if err != nil { return nil, err } pool := x509.NewCertPool() if !pool.AppendCertsFromPEM(caPEM) { return nil, errString("no certificates parsed from FLEET_MGR_ADMIN_CLIENT_CA") } return &tls.Config{ MinVersion: tls.VersionTLS12, Certificates: []tls.Certificate{cert}, ClientAuth: tls.RequireAndVerifyClientCert, ClientCAs: pool, }, nil } // ---- tiny helpers --------------------------------------------------------- func env(k, def string) string { if v := os.Getenv(k); v != "" { return v } return def } func envInt(k string, def int) int { if v := os.Getenv(k); v != "" { if n, err := strconv.Atoi(v); err == nil { return n } } return def } type errString string func (e errString) Error() string { return string(e) }