From 64da073787a41dc64467ab487f5a52fa1ceea1dd Mon Sep 17 00:00:00 2001 From: George Lambert Date: Wed, 14 Oct 2026 01:30:52 -0400 Subject: [PATCH] docs: PRODUCTION-PKI-REFUSE matrix 501 vs lab 201 Operator-facing refuse matrix for enroll/csr paths. --- docs/PKI-MINT-BACKLOG.md | 1 + docs/PRODUCTION-PKI-REFUSE.md | 80 +++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 docs/PRODUCTION-PKI-REFUSE.md diff --git a/docs/PKI-MINT-BACKLOG.md b/docs/PKI-MINT-BACKLOG.md index 48b8fe0..7d26b61 100644 --- a/docs/PKI-MINT-BACKLOG.md +++ b/docs/PKI-MINT-BACKLOG.md @@ -145,6 +145,7 @@ If product later needs in-app production mint/renew, that is a **new** design (r ## Related docs +- [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) diff --git a/docs/PRODUCTION-PKI-REFUSE.md b/docs/PRODUCTION-PKI-REFUSE.md new file mode 100644 index 0000000..2515a91 --- /dev/null +++ b/docs/PRODUCTION-PKI-REFUSE.md @@ -0,0 +1,80 @@ +# Production PKI refuse matrix (501 vs lab 201) + +**Purpose:** single place for what **stays honest 501** in production vs what may return **lab-only 201**. Live production mint is **not shipped**. Lab path is never production. + +Canonical detail: [ENROLL.md](../ENROLL.md) · backlog: [PKI-MINT-BACKLOG.md](./PKI-MINT-BACKLOG.md) · lab design: [TEST-CA-DESIGN.md](./TEST-CA-DESIGN.md) · revoke design: [CRL-OCSP-DESIGN.md](./CRL-OCSP-DESIGN.md). + +## Gate summary + +| Gate | Production / default | Lab opt-in | +|------|----------------------|------------| +| `LAB_CA_ENABLE` | unset / off | `=1` | +| `NODE_ENV=production` | **refuses lab** even if flag set | must **not** be production | +| Lab PKI dir (`LAB_PKI_DIR` or `./lab-pki`) | ignored | must exist + `init`’d (`ca.crt` + key) | +| Signer | none | `scripts/lab-test-ca.sh sign` (openssl) | +| Response markers | no cert body | `X-Lab-CA: 1`, `X-Lab-CA-Warning: NOT-FOR-PRODUCTION`, body `lab: true` | + +Fail closed: missing flag, missing lab-pki, missing script, production env, or sign failure → **no silent success**. + +## HTTP matrix — stays 501 vs lab 201 + +| Surface | Condition | HTTP | Body / headers | Production? | +|---------|-----------|------|----------------|-------------| +| `GET /enroll/csr/schema` | always | **200** | fields only; `mint: false` | OK (no crypto) | +| `GET /enroll/status` | default | **200** | `csrPostStatus: **501**`; `labCa.enabled: false` | OK | +| `GET /enroll/status` | lab ready | **200** | `csrPostStatus: **201**` (probe only); `labCa.enabled/ready` | Lab probe only — **not** a mint | +| `POST /enroll/csr` | missing / empty `csr` | **501** | `error: not_implemented` + `nextSteps` | **Stays 501** | +| `POST /enroll/csr` | invalid PEM shape | **400** | `error: invalid_csr_pem` | Shape gate (no mint) | +| `POST /enroll/csr` | valid-looking PEM, lab **off** | **501** | `not_implemented` | **Stays 501** | +| `POST /enroll/csr` | valid PEM, lab on, pki missing / not ready | **501** | `not_implemented` | **Stays 501** | +| `POST /enroll/csr` | valid PEM, lab on, `NODE_ENV=production` | **501** | lab gated off → scaffold | **Stays 501** (refuse) | +| `POST /enroll/csr` | valid PEM, lab on + pki ready + non-prod | **201** | `certificate` (+ optional `chain`); `X-Lab-CA: 1` | **LAB ONLY** | +| `POST /enroll/csr` | lab path, openssl sign fails | **502** | `lab_ca_sign_failed` + `X-Lab-CA: 1` | Lab failure (not production mint) | +| Login / My security UI | any | — | status chips / checklist only | No download-cert / mint button | +| `PUT /users/:username` `{ mtlsEnrolled }` | admin | **200** | flag only | OK (no cert issue) | +| Proxy mTLS headers | edge | — | presence flags on status | External PKI SoT | + +## What always stays refused in production (no 201) + +These must **never** return success cert material under production defaults: + +| Capability | Status | +|------------|--------| +| Production CA / HSM / EST / ACME / SCEP mint | **Not shipped** → default POST **501** | +| Silent or default-on lab mint | **Forbidden** (`LAB_CA_ENABLE` default off; production env refuses) | +| Demo / stub / self-signed “real” staff cert from IAM | **Forbidden** | +| PKCS#12 / private key return | **Never** | +| Production CRL or OCSP from staff-iam | **Not shipped** (design only; lab empty `crl` offline) | +| Dropping default 501 without real `Signer` | **Forbidden** until live PKI checklist done | + +## Operator path (production) + +1. Issue workstation client certs in **external** reverse-proxy / internal PKI — not this app. +2. Do not call `POST /enroll/csr` expecting a production cert — it returns **501**. +3. Terminate mTLS at the proxy; forward verify/DN headers. +4. Confirm `GET /enroll/status` → `proxyHeadersSeen` / `mtlsPresent`. +5. Admin `mtlsEnrolled: true` → optional `STAFF_MTLS_ENFORCE=1`. + +## Lab path (explicit only) + +```sh +./scripts/lab-test-ca.sh init --dir ./lab-pki +LAB_CA_ENABLE=1 npm start # never with NODE_ENV=production +# POST /enroll/csr { "csr": "-----BEGIN CERTIFICATE REQUEST-----..." } +# → 201 + X-Lab-CA: 1 OR 502 on sign failure OR 501 if pki not ready +``` + +## Still left (live production PKI) + +- Choose real CA model (external ACME/EST/HSM/delegated signer) +- AuthZ + full CSR ASN.1 / PoP / CN-SAN policy +- Bind cert → IAM user; metadata persist; renew/revoke +- Production CRL/OCSP (not lab empty CRL) +- Remove default 501 only after real `Signer`; keep no-fake-crypto tests + +## Related + +- [PKI-MINT-BACKLOG.md](./PKI-MINT-BACKLOG.md) — shipped surface + live checklist +- [TEST-CA-DESIGN.md](./TEST-CA-DESIGN.md) — lab script + gated HTTP +- [CRL-OCSP-DESIGN.md](./CRL-OCSP-DESIGN.md) — future revocation +- [ENROLL.md](../ENROLL.md) — states, headers, nextSteps