verae-staff-iam/docs/PKI-MINT-BACKLOG.md
George Lambert 7e8fc7c1f9 docs: PKI READY packages — prod CA cutover, AuthZ enroll, CRL/OCSP deploy
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
2026-10-14 04:20:24 -04:00

13 KiB

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 → 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. Service overview and security gates: 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.

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):
{
  "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):

  • POST /enroll/csr PEM header-shape validation → 400 invalid_csr_pem / 501 not_implemented
  • Audit event csr_attempt without storing PEM
  • Tests: invalid PEM → 400; valid-looking PEM → 501; audit flags only
  • 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):

  • 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)
  • Opt-in HTTP: LAB_CA_ENABLE=1 + lab-pki ready → POST /enroll/csr shells out to sign → 201 + certificate PEM + X-Lab-CA: 1
  • Default off → HTTP path stays 501; NODE_ENV=production refuses lab path
  • Documented in ENROLL.md + TEST-CA-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.
  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):

  • Publish future CSR body field schema (GET /enroll/csr/schema) — shipped (fields only; no mint)
  • 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
  • CRL/OCSP or proxy-side revocation story documented — 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 or a future gated test CA — see 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 “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); 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 Production CA mint/renew/revoke
AUTHZ-ENROLL-DESIGN.md Drop default 501 for real AuthZ path
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.