11 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 } fieldsdescribe names/types only (csrrequired PEM PKCS#10; optionaldeviceId,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
csrvalue:- 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
- Invalid PEM armor (missing
- Missing
csr/ empty body → HTTP 501 scaffold (discoverability unchanged) - Audit: every attempt appends
action: "csr_attempt"withdetailflags 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 includescertificate(+ optionalchain) withlab: trueand 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/csrPEM header-shape validation → 400invalid_csr_pem/ 501not_implemented- Audit event
csr_attemptwithout 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); loud NOT FOR PRODUCTION warnings; default outputlab-pki/(gitignored)- Opt-in HTTP:
LAB_CA_ENABLE=1+ lab-pki ready →POST /enroll/csrshells out tosign→ 201 +certificatePEM +X-Lab-CA: 1 - Default off → HTTP path stays 501;
NODE_ENV=productionrefuses lab path - Documented in ENROLL.md + TEST-CA-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)
- Issue workstation client certs in your external reverse-proxy / internal PKI (not this app). Do not call
POST /enroll/csrexpecting a cert — it returns 501. - Terminate mTLS at the proxy; forward verify/DN (and optional PEM presence) headers per ENROLL.md.
- Confirm
GET /enroll/statusshowsproxyHeadersSeen.any/mtlsPresentwhen the browser presents the cert. - Admin sets
mtlsEnrolled: true. - 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 (— shipped (fields only; no mint)GET /enroll/csr/schema)PEM header-shape reject on— TODO-7 scaffold only; still 501 on valid shape (no mint)POST /enroll/csr(400) +csr_attemptaudit without PEM- 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
- Remove 501 scaffold; keep “no fake crypto” rule for tests (fixtures may use offline
scripts/lab-test-ca.shor 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
- No fake production crypto — default
POST /enroll/csrstays honest 501. Lab 201 is allowed only withLAB_CA_ENABLE=1, real openssl sign vialab-test-ca.sh, andX-Lab-CA: 1headers — never silent success, never underNODE_ENV=production. - Proxy is source of truth for “cert present on this request”; IAM flag is source of truth for “user is enrolled for enforce.”
- Do not turn on
STAFF_MTLS_ENFORCEuntil the proxy actually verifies clients (see ENROLL.md “Do not”). - Never enable lab CA by default — no boot-time sign, no production image with
LAB_CA_ENABLE=1. - TOTP remains orthogonal (TOTP.md); authenticator enroll is real crypto (RFC 6238) and is unrelated to device-cert mint.
Related docs
- TEST-CA-DESIGN.md — lab script (offline) + future gated test CA design; no HTTP fake mint
scripts/lab-test-ca.sh— OpenSSL lab CA init/sign (NOT production; optionalLAB_CA_ENABLE=1HTTP shell-out)- ENROLL.md — mTLS states,
nextSteps,proxyHeadersSeen, proxy header contract, admin workflow - README.md — install, usage, staff security gates (mTLS row: proxy headers only, no device-cert mint)
- TOTP.md — authenticator 2FA (orthogonal gate)