fleet-manager: checkpoint 3 — authorise pillar (broker fold-in, births inventory)

The AUTHORISE pillar: the manager owns the enrolment queue UX, the enrol-broker
stays the state machine + step-ca signer (v1 'alongside' fold-in).

enrol-broker (minimal, backward-compatible change):
- admin approve/deny now return the decided Enrolment as JSON when the caller
  sends Accept: application/json. Browsers still redirect; other API callers
  (verify-serving.sh) still get 204. The cert PEM stays unexported (only the
  requesting host gets it via /status) — the JSON carries cert_serial, hostnqn,
  context, fingerprint, cn.

fleet-manager:
- broker.go: mTLS client for the broker admin plane (Pending/Approve/Deny),
  nil when unconfigured so the manager runs standalone.
- admin.go: GET /api/enrol/pending (proxy), POST /api/enrol/{id}/approve
  (drives broker, then BIRTHS the inventory record — identity from the returned
  serial/hostnqn/context, binding from the operator's optional body, enrolled
  with the HUMAN approver = the manager's admin mTLS CN), POST .../deny.
  503 when the broker isn't wired.

Test: httptest mock broker asserts the Accept:application/json approve, pending
proxy, inventory birth (identity+binding+enrolled), and the disabled-503 path.
Broker + manager both build/vet/test/gofmt clean.
This commit is contained in:
Audiobuzz
2026-07-04 03:51:26 +00:00
parent 80cbb833cc
commit df4fa95bdd
6 changed files with 367 additions and 14 deletions
+26 -3
View File
@@ -36,6 +36,12 @@ type config struct {
agentKey string
agentClientCA string // FLEET CA (verifies host TPM identity certs)
// enrol-broker admin plane (the authorise pillar drives it over mTLS).
brokerAdminURL string
brokerClientCert string
brokerClientKey string
brokerCA string
inventoryPath string
seed bool
hostnqnBase string
@@ -54,6 +60,11 @@ func loadConfig() config {
agentKey: env("FLEET_MGR_AGENT_KEY", ""),
agentClientCA: env("FLEET_MGR_AGENT_CLIENT_CA", ""),
brokerAdminURL: env("FLEET_MGR_BROKER_URL", ""),
brokerClientCert: env("FLEET_MGR_BROKER_CLIENT_CERT", ""),
brokerClientKey: env("FLEET_MGR_BROKER_CLIENT_KEY", ""),
brokerCA: env("FLEET_MGR_BROKER_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-"),
@@ -62,9 +73,10 @@ func loadConfig() config {
// server bundles the shared dependencies the handlers close over.
type server struct {
cfg config
store Store
hub *agentHub // live agent streams; nil if the agent plane isn't configured
cfg config
store Store
hub *agentHub // live agent streams; nil if the agent plane isn't configured
broker *brokerClient // enrol-broker admin client; nil if authorise isn't configured
}
func main() {
@@ -83,6 +95,17 @@ 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 {
log.Printf("authorise pillar DISABLED (set FLEET_MGR_BROKER_URL + client cert/key + CA to enable)")
}
// 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.