Enroll devices and IoT fleets
!!! success "Served path — wiring census 81/81"
The Definition-of-Done census reports **81/81 required capabilities served**: **12 of 81 census rows launch the shipped binary** and **69 of 81 are proved through the production-assembled handler**.
Production-assembled means production `buildRunDeps` output driving the assembled `Server.Handler` in-process, with a hand-built `Deps` rejected; only the process launch differs.
Independently proof-gated capability rows used by this journey (all `required`, all `served`): `pqc_end_to_end.pure_mldsa_leaf_stock_clients`, `protocol_ergonomics.eval_profile`.
Core surfaces guarded by route and journey tests: `est_protocol`, `scep_protocol`, `cmp_protocol`, `device_bootstrap`.
This badge is generated from `wiring-census.json`; `make journey-census-check` fails closed if the census or this page drifts.
Goal
When you finish this journey, the routers, switches, printers, phones, and IoT devices you already run will enroll for trstctl-issued certificates over the enrollment protocols baked into their firmware — and renew before expiry — without re-flashing a single device. It is for teams with an existing fleet of network or embedded gear that speaks EST, SCEP, or CMP. In plain terms: a device asks trstctl for a certificate using the protocol it already knows, trstctl checks the request against its rules, and hands back a signed certificate.
Before you start
- A running trstctl control plane with a provisioned issuing CA. Bring one up via Getting started.
- A device (or a standard client such as a stock EST client) that speaks EST, SCEP, or CMP.
- For depth on the protocols, certificate-profile control, and failure behavior, see Enrollment protocols.
Steps
Enable the enrollment protocol your fleet speaks. Each protocol server is off by default and binds to a tenant. For EST, turn it on:
protocols: est: enabled: true tenant_id: "11111111-1111-1111-1111-111111111111"You should see the control plane mount EST under
/.well-known/est/...(and, similarly, SCEP at/scepand CMP at/cmpwhen enabled) on startup. The three protocols and what each industry uses are covered in Enrollment protocols.Fetch the CA chain to establish trust. A device fetches the CA chain first (no auth) so it can bootstrap explicit trust before sending anything:
curl -s https://trstctl.example.com/.well-known/est/cacerts -o cacerts.p7You should receive a certs-only PKCS#7 chain. The device installs it as its explicit TLS trust anchor.
Enroll: POST a CSR, get back a certificate. The device generates its key locally (the private half never crosses the wire), builds a PKCS#10 CSR, and enrolls. With a stock client this is a base64 CSR POSTed to
/simpleenroll:curl -s -H "Content-Type: application/pkcs10" \ -H "Idempotency-Key: $(uuidgen)" \ --data-binary @request.b64 \ https://trstctl.example.com/.well-known/est/simpleenrollYou should get back the issued certificate wrapped in a PKCS#7. Every enrollment is validated against the endpoint's bound certificate profile before anything is signed — a disallowed key type, EKU, over-long validity, or out-of-profile name is rejected. The profile model is described in Issuance & certificate authorities.
Renew before expiry. Before the certificate expires, the device re-enrolls over the same protocol — for EST that is a
POSTto/simplereenroll, the same request and response shapes as the first enroll. You should see a fresh certificate issued the same way. Because issuance is idempotent, a retried enrollment never mints two certificates.For the smallest devices, bootstrap with a one-time token. Constrained IoT hardware that cannot run a full agent bootstraps with a single-use token over the served endpoint; the device generates and keeps its own key and sends only a CSR:
curl -s -X POST https://trstctl.example.com/enroll/bootstrap \ -d '{"token":"<one-time-token>","csr":"<base64-DER-CSR>"}' # -> {"certificate":"<PEM chain>"}You should receive a PEM certificate chain. The token is checked-and-deleted atomically, so it works exactly once.
For MDM-managed phones and laptops, gate enrollment with a challenge. When an Intune deployment pushes a SCEP profile, only devices with an MDM-signed JWS in the CSR's
challengePasswordmay enroll. trstctl verifies that signature against the enabled policy's referenced public certificate, requires the expected audience, checks tenant and CSR claims, and atomically consumes the nonce in PostgreSQL before issuing. Missing or reused challenges fail closed, including after a control-plane restart. This is covered in Enrollment protocols.Served boundary: EST, SCEP, CMP, embedded bootstrap, and embedded renewal are cross-checked end to end. With the Enterprise/PQC license attached, the same EST route accepts an RFC 9881 ML-DSA-65 CSR produced by stock OpenSSL 3.5 and returns a pure ML-DSA leaf that stock OpenSSL verifies. The local Intune-style JWS enrollment path is served and uses enabled policy
secret://public trust anchors at validation time;protocols.scep.intune_challengeremains a bootstrap fallback. A local SCEP client and JWS signer do not prove Microsoft Intune installation or Jamf integration. Enabledhmac-dynamicand Jamf policies are blocked by policy preview and create/update because the served SCEP gate cannot validate them. They may be staged disabled, but they cannot authorize live enrollment until a tenant-bound validator and durable replay fence are implemented.
Where next
Journey: J4 Steps through: F22, F23, F55, F54, F56