fleet-manager: checkpoint 2 — gRPC agent plane (bidi mTLS stream)
The steady-state host<->manager transport, resolving the design's OPEN FORK to bespoke gRPC (not OTLP+Wazuh). One long-lived, host-dialed, mTLS bidi stream: health/metrics up, desired-state/commands down — no polling, real-time control, still no inbound host port. - proto/fleet_agent.proto: FleetAgent.Session(stream AgentUp) -> stream ManagerDown. AgentUp = Hello | HealthSnapshot | CommandResult; ManagerDown = DesiredState | Command(reboot/drain/resync). int64 unix times (no well-known-type dep). Generated stubs committed (CI needs no protoc). - agent.go: gRPC server with mTLS verified by the FLEET CA (the host's reused TPM identity cert — distinct from the admin plane's admin CA). Identity is the peer cert SERIAL, taken from the verified cert = inventory key, never self-asserted; an unenrolled serial is refused (enrolment gates the stream). Per-conn send channel decouples admin pushes from stream.Send. Stream registry (serial->conn) with reconnect eviction. On connect + on change, desired-state is pushed live; inventory stays authoritative so offline hosts reconcile on reconnect. Metrics go through a MetricsSink seam (no-op now; InfluxDB in ckpt 4). - main.go: agent plane starts only when its cert/key/CA are set; admin can run standalone. Lifecycle PUT now live-pushes and reports pushed_live. Full mTLS integration test (in-process fleet CA + host client cert): initial desired-state on connect, Hello+snapshot -> inventory + online presence, live desired-state push, and unenrolled-serial rejection. go build/test/vet/gofmt clean. Deps (server-side only): google.golang.org/grpc + protobuf.
This commit is contained in:
@@ -30,6 +30,12 @@ type config struct {
|
||||
adminCert string
|
||||
adminKey string
|
||||
adminClientCA string // dedicated admin CA (verifies admin browser certs)
|
||||
|
||||
agentAddr string
|
||||
agentCert string
|
||||
agentKey string
|
||||
agentClientCA string // FLEET CA (verifies host TPM identity certs)
|
||||
|
||||
inventoryPath string
|
||||
seed bool
|
||||
hostnqnBase string
|
||||
@@ -42,6 +48,12 @@ func loadConfig() config {
|
||||
adminCert: env("FLEET_MGR_ADMIN_CERT", ""),
|
||||
adminKey: env("FLEET_MGR_ADMIN_KEY", ""),
|
||||
adminClientCA: env("FLEET_MGR_ADMIN_CLIENT_CA", ""),
|
||||
|
||||
agentAddr: env("FLEET_MGR_AGENT_ADDR", "127.0.0.1:9600"),
|
||||
agentCert: env("FLEET_MGR_AGENT_CERT", ""),
|
||||
agentKey: env("FLEET_MGR_AGENT_KEY", ""),
|
||||
agentClientCA: env("FLEET_MGR_AGENT_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-"),
|
||||
@@ -52,6 +64,7 @@ func loadConfig() config {
|
||||
type server struct {
|
||||
cfg config
|
||||
store Store
|
||||
hub *agentHub // live agent streams; nil if the agent plane isn't configured
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -70,6 +83,20 @@ func main() {
|
||||
|
||||
s := &server{cfg: cfg, store: store}
|
||||
|
||||
// Agent plane (gRPC/mTLS, fleet CA). Started only when configured; the admin
|
||||
// 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.
|
||||
if cfg.agentCert != "" && cfg.agentKey != "" && cfg.agentClientCA != "" {
|
||||
s.hub = newAgentHub(store, nil) // InfluxDB sink wired in checkpoint 4
|
||||
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)")
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
s.routes(mux)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user