initial tpm broker
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
# 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`](../../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 sign`s 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`](../dot1x-bench/docker-compose.yml)
|
||||
alongside step-ca. From `serving/dot1x-bench/`:
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
```bash
|
||||
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.
|
||||
Reference in New Issue
Block a user