Convert OPEN-ISSUES #4 external items to READY ops packages without shipping production mint. LAB_CA_ENABLE stays default off; NODE_ENV= production still refuses lab; default POST /enroll/csr remains 501. - docs/PROD-CA-CUTOVER-RUNBOOK.md (mint/renew/revoke cutover) - docs/AUTHZ-ENROLL-DESIGN.md (gates before dropping 501) - docs/CRL-OCSP-DEPLOY-CHECKLIST.md (proxy/CA revoke deploy) - Cross-links from README, ENROLL, PKI-MINT-BACKLOG, refuse/design docs npm test: 19/19 PASS
169 lines
13 KiB
Markdown
169 lines
13 KiB
Markdown
# PKI mint backlog (device certificates)
|
|
|
|
**Status: not shipped.** Live device-certificate **mint** and **renew** are **out of scope** for `verae-staff-iam` today. This service does not run a CA, sign CSRs, issue PKCS#12/PEM client certs, renew expiry, or perform any local cert crypto. There is **no fake PKI**.
|
|
|
|
What *is* shipped is **proxy-only enroll/status**, a **CSR field schema** (`GET /enroll/csr/schema`), an **honest CSR scaffold** that **defaults to 501**, plus an **opt-in LAB ONLY** path (`LAB_CA_ENABLE=1` + lab-pki → shell-out [`lab-test-ca.sh`](../scripts/lab-test-ca.sh) → **201** + `X-Lab-CA: 1`). Production mint is still not shipped. Certificate lifecycle for real operators stays at the edge PKI / reverse proxy.
|
|
|
|
Canonical behavior and header contract: [ENROLL.md](../ENROLL.md). Service overview and security gates: [README.md](../README.md).
|
|
|
|
## Shipped today (proxy-only + CSR schema + CSR scaffold)
|
|
|
|
| Surface | What it does | What it does not do |
|
|
|---------|----------------|---------------------|
|
|
| `GET /enroll/status` | Returns `state`, `stateLabel`, `note`, `updatedAt`, `nextSteps[]`, `csrSchemaUrl`, `csrPostStatus` (**501** default; **201** when lab CA ready), `labCa` probe, `proxyHeadersSeen`, `proxy`, mTLS + TOTP flags | No production cert body; lab warning only when lab enabled |
|
|
| `GET /enroll/csr/schema` | **200** JSON: future `POST /enroll/csr` body **fields only** (`csr`, `deviceId`, `commonName`, `sans`); `mint: false`, `status: "schema_only"` | No sample CSR, no PEM/PKCS#12, no mint |
|
|
| `POST /enroll/csr` | **Default 501** scaffold + PEM shape gate; **lab opt-in** `LAB_CA_ENABLE=1` + lab-pki → shell-out sign → **201** + `X-Lab-CA: 1` (never production / refused when `NODE_ENV=production`); invalid PEM → `400`; audit `csr_attempt` (no CSR PEM stored) | No production CA; lab path is explicit and headed |
|
|
| `mint` / `renew` fields on status | **Copy only** — tell operators mint/renew happen at proxy/PKI | Not crypto |
|
|
| `not_enrolled` `nextSteps` | Mentions the CSR scaffold (`501`) so UIs do not expect in-app mint | Still directs operators to external PKI |
|
|
| `PUT /users/:username` `{ mtlsEnrolled }` | Admin enrollment flag in the IAM store | Does not issue or revoke a cert |
|
|
| `STAFF_MTLS_ENFORCE=1` | Login requires proxy cert headers when the user is enrolled | Does not verify TLS itself |
|
|
| Login + My security UI | Banner/chips/checklist from `/enroll/status` | No “download cert” or mint button |
|
|
|
|
`state` values remain: `not_enrolled` → `pending_proxy` → `enrolled` → `enforce_on`. Full matrix and `nextSteps` by state: [ENROLL.md](../ENROLL.md).
|
|
|
|
### CSR schema (`GET /enroll/csr/schema`)
|
|
|
|
Fields-only contract for a **future** mint body. **Does not mint.**
|
|
|
|
- Method/path: `GET /enroll/csr/schema`
|
|
- Response: **HTTP 200**
|
|
- Body: `{ ok, path: "POST /enroll/csr", mint: false, status: "schema_only", fields: [...], notReturned: [...], nextSteps }`
|
|
- `fields` describe names/types only (`csr` required PEM PKCS#10; optional `deviceId`, `commonName`, `sans`) — no sample CSR material.
|
|
- Auth optional; allowed under the TOTP enrollment allow-list.
|
|
|
|
### CSR scaffold (`POST /enroll/csr`)
|
|
|
|
Reserved path for production live mint. **Default must not succeed (501).** Lab opt-in may return 201 with `X-Lab-CA: 1` only.
|
|
|
|
- Method/path: `POST /enroll/csr`
|
|
- **PEM header-shape gate (TODO-7):** if JSON body includes a non-empty `csr` value:
|
|
- **Invalid** PEM armor (missing `BEGIN`/`END CERTIFICATE REQUEST`, empty body between armor, private-key labels, non-string) → **HTTP 400** `{ error: "invalid_csr_pem", message, nextSteps }`
|
|
- **Valid-looking** PEM shape only (headers + non-empty middle; **no** ASN.1/crypto verify) → **HTTP 501** scaffold below
|
|
- Missing `csr` / empty body → **HTTP 501** scaffold (discoverability unchanged)
|
|
- Audit: every attempt appends `action: "csr_attempt"` with `detail` flags only (`outcome=…; pemShape=absent|ok|invalid; signedIn=…`). **PEM text is never stored** in audit, logs, or user records.
|
|
- Scaffold **501** body (JSON only):
|
|
|
|
```json
|
|
{
|
|
"error": "not_implemented",
|
|
"nextSteps": [
|
|
"Live device-certificate mint is not shipped in staff-iam; POST /enroll/csr always returns 501.",
|
|
"GET /enroll/csr/schema returns the future request-body fields only (no mint).",
|
|
"Issue workstation client certificates in your reverse-proxy / internal PKI (not this service).",
|
|
"Confirm GET /enroll/status shows proxy.verifyOk or proxy.hasDn / proxy.hasPem when the browser presents the cert.",
|
|
"Have an IAM admin set mtlsEnrolled: true via PUT /users/:username, then optionally STAFF_MTLS_ENFORCE=1.",
|
|
"See docs/PKI-MINT-BACKLOG.md for what a real CA-backed mint would require."
|
|
]
|
|
}
|
|
```
|
|
|
|
- No CSR ASN.1 parse, no sign, no storage of PEM body, no cert material returned.
|
|
- Default scaffold responses never include `certificate` / `pem` / `pkcs12`. Lab 201 includes `certificate` (+ optional `chain`) with `lab: true` and warning text only.
|
|
- Auth optional; same status rules whether anonymous or signed-in.
|
|
- Allowed under the TOTP enrollment allow-list so a pending-2FA session can still discover the 501 / shape gate.
|
|
|
|
When production mint ships, replace the default 501 with real CA integration (see “Left for live PKI” below). Until then, do not soften the **default** 501 or return demo material without the lab gate + `X-Lab-CA` headers.
|
|
|
|
### TODO-7 slice (MULTI-AGENT-TODO-7 A4)
|
|
|
|
Shipped this pass (still **no mint**):
|
|
|
|
- [x] `POST /enroll/csr` PEM header-shape validation → **400** `invalid_csr_pem` / **501** `not_implemented`
|
|
- [x] Audit event `csr_attempt` without storing PEM
|
|
- [x] Tests: invalid PEM → 400; valid-looking PEM → 501; audit flags only
|
|
- [x] This backlog section
|
|
|
|
Still left for live PKI: full checklist under “Left for live PKI” below.
|
|
|
|
### Lab CA script + opt-in HTTP (P0 Issue 4 — still **no production mint**)
|
|
|
|
Shipped for **lab/dev only** (loud warnings; never production):
|
|
|
|
- [x] [`scripts/lab-test-ca.sh`](../scripts/lab-test-ca.sh) — OpenSSL self-signed lab root (`init`) + sign CSR → leaf PEM/chain (`sign`); optional empty **`crl`**; loud **NOT FOR PRODUCTION** warnings; default output `lab-pki/` (gitignored)
|
|
- [x] **Opt-in HTTP:** `LAB_CA_ENABLE=1` + lab-pki ready → `POST /enroll/csr` shells out to `sign` → **201** + `certificate` PEM + `X-Lab-CA: 1`
|
|
- [x] **Default off** → HTTP path stays **501**; `NODE_ENV=production` refuses lab path
|
|
- [x] Documented in [ENROLL.md](../ENROLL.md) + [TEST-CA-DESIGN.md](./TEST-CA-DESIGN.md) + [CRL-OCSP-DESIGN.md](./CRL-OCSP-DESIGN.md)
|
|
|
|
| Layer | Lab path | Production mint (backlog below) |
|
|
|-------|----------|----------------------------------|
|
|
| Default today | Flag off → **501** | Not shipped |
|
|
| Lab opt-in | `LAB_CA_ENABLE=1` + pki → **201** + `X-Lab-CA: 1` | Must not use lab CA |
|
|
| When live mint ships | Keep as CI/dev fixture | Real `Signer` (HSM/EST/ACME/delegated) |
|
|
| Never | Default-on or production `NODE_ENV` | — |
|
|
|
|
Operators continue to issue workstation certs at external reverse-proxy / internal PKI.
|
|
|
|
### Operator path (current)
|
|
|
|
1. Issue workstation client certs in your **external** reverse-proxy / internal PKI (not this app). Do not call `POST /enroll/csr` expecting a cert — it returns 501.
|
|
2. Terminate mTLS at the proxy; forward verify/DN (and optional PEM presence) headers per [ENROLL.md](../ENROLL.md).
|
|
3. Confirm `GET /enroll/status` shows `proxyHeadersSeen.any` / `mtlsPresent` when the browser presents the cert.
|
|
4. Admin sets `mtlsEnrolled: true`.
|
|
5. Only then consider `STAFF_MTLS_ENFORCE=1`.
|
|
|
|
Renewal before expiry is the same external PKI/proxy path. staff-iam never sees private keys or issued cert material beyond optional header presence flags.
|
|
|
|
## Not shipped (backlog — live mint/renew)
|
|
|
|
The following are **explicitly not implemented** and must not be faked in UI or API. The CSR route exists only as a **501 scaffold**.
|
|
|
|
- Live CSR intake and signing (body accepted and processed, not discarded)
|
|
- Certificate issuance (PEM, DER, PKCS#12 download)
|
|
- Automated or on-demand **renew** / re-key
|
|
- Local CA, intermediate, or self-signed device-cert generation inside this process
|
|
- Parsing forwarded PEM for expiry, SAN, or username binding
|
|
- Revocation lists or OCSP from staff-iam
|
|
- Any “demo” or stub cert that pretends to be a real device credential
|
|
|
|
### Left for live PKI
|
|
|
|
Checklist for a real in-app mint (product + security design required before coding):
|
|
|
|
- [x] ~~Publish future CSR body field schema (`GET /enroll/csr/schema`)~~ — shipped (fields only; no mint)
|
|
- [x] ~~PEM header-shape reject on `POST /enroll/csr` (400) + `csr_attempt` audit without PEM~~ — TODO-7 scaffold only; still 501 on valid shape (no mint)
|
|
- [ ] Choose CA model: external ACME/EST/SCEP, internal HSM-backed CA, or delegated signer service
|
|
- [ ] AuthN/AuthZ for `POST /enroll/csr` (session + role/permission; rate limits beyond audit)
|
|
- [ ] CSR validation (full PEM/ASN.1 parse, key type/size, CN/SAN policy, proof-of-possession)
|
|
- [ ] Bind issued cert identity to IAM user (and optional device id) without trusting client-supplied CN alone
|
|
- [ ] Issue path: sign → return cert chain (PEM) and/or PKCS#12; never log private keys
|
|
- [ ] Persist serial / fingerprint / notAfter for status and renew gates (store metadata only)
|
|
- [ ] `GET /enroll/status` (or successor) surfaces real cert metadata when present
|
|
- [ ] Renew / re-key endpoint and expiry warnings; revoke on offboard
|
|
- [x] ~~CRL/OCSP or proxy-side revocation story documented~~ — [CRL-OCSP-DESIGN.md](./CRL-OCSP-DESIGN.md); lab empty CRL via `lab-test-ca.sh crl` (not production)
|
|
- [ ] Remove 501 scaffold; keep “no fake crypto” rule for tests (fixtures may use offline [`scripts/lab-test-ca.sh`](../scripts/lab-test-ca.sh) or a future gated test CA — see [TEST-CA-DESIGN.md](./TEST-CA-DESIGN.md); **never** default-on in server)
|
|
- [ ] Operator runbooks + ENROLL.md / README updated for the live path
|
|
|
|
If product later needs in-app production mint/renew, that is a **new** design (real CA integration or delegated signer), not the lab CA path. Until then, keep operator mint/renew copy pointing at the proxy/PKI, keep default `POST /enroll/csr` as 501, and keep lab mint behind `LAB_CA_ENABLE` + `X-Lab-CA: 1`.
|
|
|
|
## Hard rules
|
|
|
|
1. **No fake production crypto** — default `POST /enroll/csr` stays honest **501**. Lab **201** is allowed only with `LAB_CA_ENABLE=1`, real openssl sign via `lab-test-ca.sh`, and `X-Lab-CA: 1` headers — never silent success, never under `NODE_ENV=production`.
|
|
2. **Proxy is source of truth** for “cert present on this request”; IAM flag is source of truth for “user is enrolled for enforce.”
|
|
3. Do not turn on `STAFF_MTLS_ENFORCE` until the proxy actually verifies clients (see [ENROLL.md](../ENROLL.md) “Do not”).
|
|
4. **Never enable lab CA by default** — no boot-time sign, no production image with `LAB_CA_ENABLE=1`.
|
|
5. TOTP remains orthogonal ([TOTP.md](../TOTP.md)); authenticator enroll is real crypto (RFC 6238) and is unrelated to device-cert mint.
|
|
|
|
## READY packages (external ops — packaged 2026-10-14)
|
|
|
|
In-repo software stays complete; live CA/AuthZ/CRL remain ops/product. These READY packages are the cutover kits:
|
|
|
|
| Package | Covers former [ext] |
|
|
|---------|---------------------|
|
|
| [PROD-CA-CUTOVER-RUNBOOK.md](./PROD-CA-CUTOVER-RUNBOOK.md) | Production CA mint/renew/revoke |
|
|
| [AUTHZ-ENROLL-DESIGN.md](./AUTHZ-ENROLL-DESIGN.md) | Drop default 501 for real AuthZ path |
|
|
| [CRL-OCSP-DEPLOY-CHECKLIST.md](./CRL-OCSP-DEPLOY-CHECKLIST.md) | Production CRL/OCSP |
|
|
|
|
**Invariant:** `LAB_CA_ENABLE` default off; `NODE_ENV=production` refuses lab; default `POST /enroll/csr` → **501** until Signer + AuthZ go criteria pass.
|
|
|
|
## Related docs
|
|
|
|
- [PROD-CA-CUTOVER-RUNBOOK.md](./PROD-CA-CUTOVER-RUNBOOK.md) — **READY** prod CA mint/renew/revoke cutover
|
|
- [AUTHZ-ENROLL-DESIGN.md](./AUTHZ-ENROLL-DESIGN.md) — **READY** AuthZ before dropping 501
|
|
- [CRL-OCSP-DEPLOY-CHECKLIST.md](./CRL-OCSP-DEPLOY-CHECKLIST.md) — **READY** CRL/OCSP deploy checklist
|
|
- [PRODUCTION-PKI-REFUSE.md](./PRODUCTION-PKI-REFUSE.md) — matrix: what stays **501** vs lab **201**; production refuse rules
|
|
- [TEST-CA-DESIGN.md](./TEST-CA-DESIGN.md) — lab script (offline) + **future** gated test CA design; no HTTP fake mint
|
|
- [CRL-OCSP-DESIGN.md](./CRL-OCSP-DESIGN.md) — future real-CA revocation (proxy CRL/OCSP); lab empty CRL only
|
|
- [`scripts/lab-test-ca.sh`](../scripts/lab-test-ca.sh) — OpenSSL lab CA init/sign/crl (NOT production; optional `LAB_CA_ENABLE=1` HTTP shell-out)
|
|
- [ENROLL.md](../ENROLL.md) — mTLS states, `nextSteps`, `proxyHeadersSeen`, proxy header contract, admin workflow
|
|
- [README.md](../README.md) — install, usage, staff security gates (mTLS row: proxy headers only, no device-cert mint)
|
|
- [TOTP.md](../TOTP.md) — authenticator 2FA (orthogonal gate)
|