Files
diskless-workstation/serving/enrol-broker/README.md
T
2026-06-28 10:33:14 +00:00

5.3 KiB

fleet-enrol-broker

The TPM identity enrolment broker — the one component that turns "step-ca can sign" into "step-ca signs only what a human approved." It is the system's single unauthenticated, pre-auth bootstrap endpoint: an empty-NV host submits a CSR, an admin verifies a fingerprint out-of-band and approves, and the broker returns a step-ca-signed cert the host writes to its TPM NV.

Authoritative design: ../../docs/tpm-enrolment-design.md. This is v1 (fingerprint-only OOB assurance; inline empty-NV vehicle on the host side). TPM attestation and the host-side dracut branch are the next phases.

Two listeners, by design

PUBLIC  (plain HTTP, behind the serving nginx /enrol/)   UNAUTHENTICATED
  POST /request        {csr, context}        -> {enrol_id, fingerprint, ...}
  GET  /status/{id}                          -> pending | approved+cert | denied | expired
  GET  /healthz

ADMIN   (mTLS, management network / loopback ONLY)        admin client cert
  GET  /              the pending-queue web UI (fingerprint + approve/deny)
  GET  /pending       JSON queue
  POST /approve/{id}  OOB-approve -> step ca sign -> store cert
  POST /deny/{id}     reject

The public side is deliberately tiny and stdlib-only — it is the only thing reachable by un-enrolled devices. The admin side never touches the boot LAN.

How identity is gated

  1. Host generates a key in the TPM, builds a CSR, POST /request.
  2. Broker stores a time-boxed PENDING record and returns a fingerprint = wordlist( SHA-256( CSR-pubkey ‖ server-nonce ) ). The nonce is server-chosen per enrol-id, so an attacker can't pre-grind a colliding fingerprint.
  3. Host shows the fingerprint on its Plymouth splash. An admin opens the mTLS web UI, cross-checks the fingerprint against the device's screen, confirms the device context (MAC / DMI / TPM EK), and approves.
  4. Broker mints a one-time step token (it — not the host — holds the provisioner password) and step ca signs the CSR; the cert is returned on the host's next GET /status/{id} poll. The hostnqn is derived from the cert serial.

Lifecycle (from the design): empty-NV → enrol (human); near-expiry → renew (automatic, not built here); auth-fail with a cert present → alert, never auto-re-enrol.

Run it (bench)

The broker ships as a service in ../dot1x-bench/docker-compose.yml alongside step-ca. From serving/dot1x-bench/:

docker compose up -d step-ca && ./step-ca/bootstrap.sh   # CA + root_ca.crt + enrol provisioner
../enrol-broker/admin-pki.sh ./pki/broker alice          # admin mTLS PKI (dedicated admin CA)
docker compose up -d enrol-broker

Then import pki/broker/clients/alice.p12 (empty password) into your browser and open https://127.0.0.1:9443/. Hosts reach the public side via the serving stack's nginx: http://boot.fleet.lab/enrol/.

Admin mTLS PKI — important

admin-pki.sh mints a dedicated admin CA, separate from the fleet CA. Do not point BROKER_ADMIN_CLIENT_CA at the fleet root: host enrolment certs are signed by that same root, so reusing it would let any enrolled host authenticate to the admin plane and approve enrolments (privilege escalation).

Configuration (env)

Var Default Notes
BROKER_PUBLIC_ADDR 127.0.0.1:9090 unauth listener (behind nginx)
BROKER_ADMIN_ADDR 127.0.0.1:9443 mTLS admin listener (mgmt net only)
BROKER_SIGNER step step (prod) or mock (dev, ephemeral CA)
STEP_CA_URL / STEP_CA_ROOT — step-ca endpoint + root
STEP_PROVISIONER / STEP_PROVISIONER_PASSWORD_FILE enrol / … the broker holds this, not hosts
STEP_CERT_NOT_AFTER 720h issued-cert validity
BROKER_PENDING_TTL 10m time-box on unapproved requests
BROKER_WORDS 8 fingerprint word count (64-word list ⇒ 6 bits/word ⇒ 48 bits)
BROKER_MAX_PENDING 256 flooding cap
BROKER_RATE_PER_MIN 12 per-IP /request rate limit
HOSTNQN_BASE nqn.2026-06.org.example.fleet:host- hostnqn = base + cert-serial
BROKER_ADMIN_CERT / _KEY / _CLIENT_CA — admin mTLS (required unless insecure)
BROKER_ADMIN_INSECURE false dev only: plaintext admin, no client-cert auth

Swap wordlist.txt for the 2048-word BIP39 list (11 bits/word) for more entropy per word; it just needs to be a power-of-two number of lines.

Dev loop without step-ca

BROKER_SIGNER=mock BROKER_ADMIN_INSECURE=true \
BROKER_PUBLIC_ADDR=127.0.0.1:9090 BROKER_ADMIN_ADDR=127.0.0.1:9443 go run .

mock signs from an ephemeral in-process CA (LAB ONLY — those certs are not trusted by RADIUS/NVMe). For an mTLS dev run instead of INSECURE, generate certs with admin-pki.sh and set BROKER_ADMIN_CERT/_KEY/_CLIENT_CA.

Not built yet (next phases)

  • Host side: the dracut empty-NV enrolment branch (generate key/CSR, POST, show fingerprint on Plymouth, poll, write NV, reboot) + tpm2-openssl in the image. ../dot1x-bench/enrol/tpm-provision.sh is the manual stand-in.
  • Renew path (automatic, near-expiry) and TPM attestation (ACME device-attest-01) at the approve step.
  • Fold into the fleet-manager: the admin routes become its authenticated plane; approval writes the shared inventory record.