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
- Host generates a key in the TPM, builds a CSR,
POST /request. - 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. - 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.
- Broker mints a one-time
steptoken (it — not the host — holds the provisioner password) andstep ca signs the CSR; the cert is returned on the host's nextGET /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-opensslin the image.../dot1x-bench/enrol/tpm-provision.shis 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.