From 7e8fc7c1f9262cf9e2406ca610c4b3b83267c740 Mon Sep 17 00:00:00 2001 From: George Lambert Date: Wed, 14 Oct 2026 04:20:24 -0400 Subject: [PATCH] =?UTF-8?q?docs:=20PKI=20READY=20packages=20=E2=80=94=20pr?= =?UTF-8?q?od=20CA=20cutover,=20AuthZ=20enroll,=20CRL/OCSP=20deploy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- ENROLL.md | 7 + README.md | 5 + docs/AUTHZ-ENROLL-DESIGN.md | 241 ++++++++++++++++++++++++++++++ docs/CRL-OCSP-DEPLOY-CHECKLIST.md | 175 ++++++++++++++++++++++ docs/CRL-OCSP-DESIGN.md | 3 + docs/PKI-MINT-BACKLOG.md | 15 ++ docs/PROD-CA-CUTOVER-RUNBOOK.md | 183 +++++++++++++++++++++++ docs/PRODUCTION-PKI-REFUSE.md | 3 + docs/TEST-CA-DESIGN.md | 11 ++ 9 files changed, 643 insertions(+) create mode 100644 docs/AUTHZ-ENROLL-DESIGN.md create mode 100644 docs/CRL-OCSP-DEPLOY-CHECKLIST.md create mode 100644 docs/PROD-CA-CUTOVER-RUNBOOK.md diff --git a/ENROLL.md b/ENROLL.md index 46ad3c3..5445762 100644 --- a/ENROLL.md +++ b/ENROLL.md @@ -232,6 +232,13 @@ staff-iam **never** parses the PEM, checks expiry, or matches CN to username. Bi ## Related docs +- [docs/PROD-CA-CUTOVER-RUNBOOK.md](./docs/PROD-CA-CUTOVER-RUNBOOK.md) — **READY** production CA mint/renew/revoke cutover +- [docs/AUTHZ-ENROLL-DESIGN.md](./docs/AUTHZ-ENROLL-DESIGN.md) — **READY** AuthZ design before dropping default 501 +- [docs/CRL-OCSP-DEPLOY-CHECKLIST.md](./docs/CRL-OCSP-DEPLOY-CHECKLIST.md) — **READY** production CRL/OCSP deploy +- [docs/PKI-MINT-BACKLOG.md](./docs/PKI-MINT-BACKLOG.md) — live mint backlog; lab vs production +- [docs/PRODUCTION-PKI-REFUSE.md](./docs/PRODUCTION-PKI-REFUSE.md) — refuse matrix +- [docs/TEST-CA-DESIGN.md](./docs/TEST-CA-DESIGN.md) — lab CA only +- [docs/CRL-OCSP-DESIGN.md](./docs/CRL-OCSP-DESIGN.md) — revocation design - [TOTP.md](./TOTP.md) — authenticator setup, `STAFF_TOTP_REQUIRED`, login gate - [README.md](./README.md) — install, usage, seed users - [SUMMARY.md](./SUMMARY.md) — one-line service summary diff --git a/README.md b/README.md index 2e60992..8f4763a 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,11 @@ Open `http://127.0.0.1:3028/login`. Persist store with `STAFF_IAM_PATH`. Login i | [ENROLL.md](./ENROLL.md) | Device cert (mTLS): status + CSR schema; default `POST /enroll/csr` → **501**; opt-in **LAB ONLY** `LAB_CA_ENABLE=1` → 201 + `X-Lab-CA:1` (never production) | | [docs/TEST-CA-DESIGN.md](./docs/TEST-CA-DESIGN.md) | Lab CA script + gated HTTP path; production mint still backlog | | [docs/PKI-MINT-BACKLOG.md](./docs/PKI-MINT-BACKLOG.md) | Live production PKI checklist | +| [docs/PROD-CA-CUTOVER-RUNBOOK.md](./docs/PROD-CA-CUTOVER-RUNBOOK.md) | **READY:** production CA mint/renew/revoke cutover (ops; no IAM CA) | +| [docs/AUTHZ-ENROLL-DESIGN.md](./docs/AUTHZ-ENROLL-DESIGN.md) | **READY:** AuthZ design before dropping default 501 | +| [docs/CRL-OCSP-DEPLOY-CHECKLIST.md](./docs/CRL-OCSP-DEPLOY-CHECKLIST.md) | **READY:** production CRL/OCSP proxy/CA deploy checklist | +| [docs/PRODUCTION-PKI-REFUSE.md](./docs/PRODUCTION-PKI-REFUSE.md) | Refuse matrix: default 501 vs lab 201; prod never lab | +| [docs/CRL-OCSP-DESIGN.md](./docs/CRL-OCSP-DESIGN.md) | Revocation architecture (design only) | | [TOTP.md](./TOTP.md) | Authenticator 2FA + **`STAFF_TOTP_REQUIRED`** enroll path; how login / app UI use flags | | [NATS.md](./NATS.md) | NATS notes (if used) | | [SUMMARY.md](./SUMMARY.md) | One-line service summary | diff --git a/docs/AUTHZ-ENROLL-DESIGN.md b/docs/AUTHZ-ENROLL-DESIGN.md new file mode 100644 index 0000000..3c84078 --- /dev/null +++ b/docs/AUTHZ-ENROLL-DESIGN.md @@ -0,0 +1,241 @@ +# AuthZ enroll design (READY package) — drop default 501 + +**Status: READY package** — design for replacing default **`POST /enroll/csr` → 501** with a real AuthZ’d mint path. +**Not implemented.** Until a production `Signer` exists, the scaffold **must** stay 501. Lab mint (`LAB_CA_ENABLE=1`) is **orthogonal** and remains default-off / production-refused. + +| Companion | Role | +|-----------|------| +| [PROD-CA-CUTOVER-RUNBOOK.md](./PROD-CA-CUTOVER-RUNBOOK.md) | Ops cutover + Signer go/no-go | +| [PKI-MINT-BACKLOG.md](./PKI-MINT-BACKLOG.md) | Live mint checklist | +| [PRODUCTION-PKI-REFUSE.md](./PRODUCTION-PKI-REFUSE.md) | Refuse matrix | +| [ENROLL.md](../ENROLL.md) | Current HTTP contract | +| [CRL-OCSP-DEPLOY-CHECKLIST.md](./CRL-OCSP-DEPLOY-CHECKLIST.md) | Revoke after issue | + +--- + +## Goal + +Define **who** may call live mint/renew/revoke, **what** must be validated before sign, and **when** it is safe to remove the default 501 scaffold — without weakening lab refuse rules. + +``` +Browser / agent + │ session cookie or Bearer staff_session + ▼ +staff-iam POST /enroll/csr + │ 1. AuthN session + │ 2. AuthZ role/permission + rate limit + │ 3. CSR PEM + ASN.1 + PoP + CN/SAN policy + │ 4. Bind subject → IAM user id (server-side) + ▼ +Signer.issue (external / HSM) ──► chain PEM + serial + │ + ▼ +IAM metadata persist (serial, fingerprint, notAfter) +audit csr_attempt / csr_issued (no PEM body) +``` + +--- + +## Current behavior (must keep until go criteria) + +| Condition | Result | +|-----------|--------| +| Default / production | **501** `not_implemented` after PEM shape gate | +| Invalid PEM shape | **400** `invalid_csr_pem` | +| `LAB_CA_ENABLE=1` + lab-pki + non-production | **201** + `X-Lab-CA: 1` (LAB ONLY) | +| `NODE_ENV=production` + `LAB_CA_ENABLE=1` | Lab **refused** → still **501** | +| Audit | `csr_attempt` flags only — **never** store CSR PEM | + +Do **not** partially “soften” 501 with demo PEMs or default-on lab. + +--- + +## AuthN requirements (live path) + +| Requirement | Detail | +|-------------|--------| +| Signed-in staff session | Cookie `staff_session` or `Authorization: Bearer` same session token | +| No anonymous mint | Live `POST /enroll/csr` **rejects 401** when unauthenticated (unlike today’s discoverability 501) | +| TOTP policy orthogonal | If `totpEnabled` / `STAFF_TOTP_REQUIRED`, existing login gates apply before mint | +| mTLS optional at mint time | Prefer mint from enrolled session; proxy client-cert may be absent during first enroll | +| Service accounts | If automation mints, dedicated IAM principal + permission — not shared admin passwords | + +**Schema discovery** may stay optionally unauthenticated: + +- `GET /enroll/csr/schema` → 200 fields-only (today) +- `GET /enroll/status` → 200 (today) + +--- + +## AuthZ requirements (live path) + +### Permission strings (proposed) + +| Permission | Who | Allows | +|------------|-----|--------| +| `staff.enroll.self` | any active staff (default grant on role catalog or all named roles) | Mint/renew **own** device cert only | +| `staff.enroll.admin` | `owner`, `iam-admin`, `admin` | Mint/renew/revoke **for another** user (break-glass / IT desk) | +| `staff.enroll.revoke` | `owner`, `iam-admin` (+ optional security role) | Call Signer.revoke + clear metadata | + +Exact permission ids are **not frozen** — register in `src/roles.js` map when implementing. Edge dual-staff catalog (`starter` / `ender` / `admin`) does **not** by itself grant enroll admin. + +### Binding rules + +1. **Self mint:** path subject = session user id; ignore client-supplied user id fields. +2. **Admin mint:** body may include `username` / `userId` target; require `staff.enroll.admin`; audit both actor and target. +3. **CN / SAN:** derived server-side from IAM username / email / device record — client `commonName` is a **hint** only (same as lab today) and must match policy or be overwritten. +4. **deviceId:** optional stable workstation id; bind in metadata; re-mint same device replaces serial (revoke old if policy says so). + +### Rate limits + +| Scope | Suggested default | On exceed | +|-------|-------------------|-----------| +| Per user self-mint | 5 / hour | **429** | +| Per admin actor | 30 / hour | **429** | +| Per IP (unauth noise) | already covered if 401; keep shape-gate cheap | — | + +Persist counters beside existing login rate-limit patterns (memory/SQLite). Audit every deny. + +--- + +## CSR validation ladder (before Signer) + +Today only **PEM armor shape** is checked. Live path must add: + +| Step | Reject | +|------|--------| +| 1. PEM armor + non-empty body | 400 `invalid_csr_pem` (shipped) | +| 2. Parse PKCS#10 ASN.1 | 400 `invalid_csr_asn1` | +| 3. Key type/size policy (e.g. RSA ≥ 2048 or P-256) | 400 `csr_key_policy` | +| 4. No private-key PEM confusion | 400 (already shape-adjacent) | +| 5. Proof-of-possession if separate from CSR signature | 400 `csr_pop_failed` | +| 6. Attribute / extension allow-list | 400 `csr_ext_policy` | +| 7. AuthZ + bind subject | 401 / 403 | +| 8. Signer.issue | 502/503 on signer failure — **fail closed** | + +**Never** fall through to lab CA when Signer fails under production. + +--- + +## HTTP contract changes (when live) + +### `POST /enroll/csr` (live) + +**Request** (extends schema fields): + +```json +{ + "csr": "-----BEGIN CERTIFICATE REQUEST-----...-----END CERTIFICATE REQUEST-----", + "deviceId": "optional-workstation-id", + "commonName": "optional-hint-only", + "sans": ["optional", "dns-or-email-hints"] +} +``` + +**Success 201** (production — **no** `X-Lab-CA`): + +```json +{ + "ok": true, + "certificate": "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----", + "chain": "-----BEGIN CERTIFICATE-----... optional ...", + "serial": "hex", + "notAfter": "ISO-8601", + "fingerprint": "sha256-hex-of-der" +} +``` + +**Errors:** 400 validation · 401 unauth · 403 forbidden · 429 rate · 501 only if Signer **feature flag off** · 502/503 signer down. + +### Lab coexistence + +| Env | Live Signer flag | LAB_CA_ENABLE | Result | +|-----|------------------|---------------|--------| +| production | off | any | **501** (lab refused) | +| production | on | any | Signer path only; lab never | +| dev/test | off | 0 | **501** | +| dev/test | off | 1 | lab **201** + `X-Lab-CA: 1` | +| dev/test | on | 1 | prefer Signer if configured; else lab — document one winner in impl PR | + +**Invariant:** `NODE_ENV=production` ⇒ lab path code returns false (`labCaEnabled()`). + +### Dropping default 501 + +Safe only when **all** hold: + +- [ ] **A-01** Signer client integrated + health check +- [ ] **A-02** AuthN required on mint +- [ ] **A-03** AuthZ permissions enforced (self vs admin) +- [ ] **A-04** Full CSR validation ladder (not shape-only) +- [ ] **A-05** Subject bind server-side +- [ ] **A-06** Rate limits on +- [ ] **A-07** Metadata persist + audit without PEM +- [ ] **A-08** Tests: unauth 401; forbid cross-user without admin; prod refuses lab; no fake cert fixtures pretending production +- [ ] **A-09** Feature flag `STAFF_CSR_MINT=1` (name TBD) default **off** for one release, then default on only after soak +- [ ] **A-10** Cutover runbook C-01…C-10 signed off — [PROD-CA-CUTOVER-RUNBOOK.md](./PROD-CA-CUTOVER-RUNBOOK.md) + +Until then keep: + +```json +{ "error": "not_implemented", "nextSteps": [ "… external PKI …" ] } +``` + +--- + +## Renew / revoke AuthZ + +| Action | AuthZ | Notes | +|--------|-------|-------| +| Renew self | `staff.enroll.self` + owns serial/deviceId | May be same `POST /enroll/csr` with existing metadata | +| Renew other | `staff.enroll.admin` | IT desk | +| Revoke self | `staff.enroll.self` or force admin-only (product choice) | Always Signer.revoke + clear metadata | +| Revoke other / offboard | `staff.enroll.revoke` | Dual-write CA + `mtlsEnrolled: false` | +| Bulk offboard | admin + automation principal | All serials for user | + +Revoke API must **not** succeed on IAM flag flip alone — see CRL package. + +--- + +## Audit events (live) + +| action | detail flags (no PEM) | +|--------|------------------------| +| `csr_attempt` | outcome, pemShape, labCa, signedIn (shipped) | +| `csr_issued` | serial, fingerprint, deviceId, targetUser, actor | +| `csr_renewed` | oldSerial, newSerial, actor | +| `csr_denied` | reason=authz\|rate\|policy, actor, target | +| `csr_revoked` | serial, reason, actor | + +--- + +## Implementation order + +1. Keep scaffold 501 + lab refuse (done). +2. Land Signer interface + feature flag **off**. +3. AuthN/AuthZ + rate limit behind flag (still 501 when flag off). +4. CSR ASN.1 validation behind flag. +5. Metadata + status fields. +6. Soak with flag on in lab/staging against **real** Signer (not lab-test-ca as production stand-in). +7. Production cutover per runbook; remove default 501 only when flag on + Signer healthy. +8. Docs: ENROLL.md success contract; README gates row. + +--- + +## Do not + +- Do **not** use lab CA as the production Signer +- Do **not** grant mint on `GET /roles/catalog` alone +- Do **not** trust client CN/SAN without server policy +- Do **not** log CSR/cert PEM +- Do **not** default `STAFF_CSR_MINT` on before soak +- Do **not** return 201 without AuthZ once live path ships + +## Related + +- [PROD-CA-CUTOVER-RUNBOOK.md](./PROD-CA-CUTOVER-RUNBOOK.md) +- [PKI-MINT-BACKLOG.md](./PKI-MINT-BACKLOG.md) +- [PRODUCTION-PKI-REFUSE.md](./PRODUCTION-PKI-REFUSE.md) +- [CRL-OCSP-DESIGN.md](./CRL-OCSP-DESIGN.md) +- [CRL-OCSP-DEPLOY-CHECKLIST.md](./CRL-OCSP-DEPLOY-CHECKLIST.md) +- [ENROLL.md](../ENROLL.md) +- [TEST-CA-DESIGN.md](./TEST-CA-DESIGN.md) diff --git a/docs/CRL-OCSP-DEPLOY-CHECKLIST.md b/docs/CRL-OCSP-DEPLOY-CHECKLIST.md new file mode 100644 index 0000000..52702a0 --- /dev/null +++ b/docs/CRL-OCSP-DEPLOY-CHECKLIST.md @@ -0,0 +1,175 @@ +# CRL / OCSP production deploy checklist (READY package) + +**Status: READY package** — operator checklist to deploy **production** revocation (CRL and/or OCSP) for staff device certificates. +**Not implemented in staff-iam.** No CRL/OCSP HTTP routes ship from this service. Lab `lab-test-ca.sh crl` emits an **empty lab-signed** CRL for local experiments only — **never** production. + +Design background: [CRL-OCSP-DESIGN.md](./CRL-OCSP-DESIGN.md). +Cutover context: [PROD-CA-CUTOVER-RUNBOOK.md](./PROD-CA-CUTOVER-RUNBOOK.md). +AuthZ revoke path: [AUTHZ-ENROLL-DESIGN.md](./AUTHZ-ENROLL-DESIGN.md). + +--- + +## Hard rules + +1. **Primary control = short leaf lifetime**; CRL/OCSP are complementary. +2. **Proxy / edge** performs chain verify **and** revocation check — not the IAM Node process. +3. **staff-iam does not serve** `GET /crl` or OCSP — prefer AIA/CRLDP URLs on the issuing CA hostnames. +4. **Lab empty CRL** is not a revoke authority; lab root must not be in production trust stores. +5. **No fake revoke** — flipping `mtlsEnrolled: false` without CA revoke is policy only, not crypto revoke. +6. **LAB_CA_ENABLE** stays default off; production refuses lab mint. + +--- + +## Choose enforcement option + +| Option | Where revoke is checked | staff-iam role | +|--------|-------------------------|----------------| +| **A. Proxy-side only (recommended first)** | nginx/Caddy/Envoy/WAF loads CRL or OCSP against client cert | Headers only; metadata later | +| **B. CA-adjacent PKI service** | Signer publishes CRL/OCSP; proxy still enforces TLS | IAM calls `Signer.revoke` | +| **C. In-process IAM OCSP/CRL** | Discouraged | Do not start here | + +Default deploy path: **A**. + +--- + +## Pre-flight (CA) + +- [ ] **R-01** Issuing intermediate (or leaf profile) embeds CRL Distribution Points and/or AIA OCSP URL +- [ ] **R-02** CRL/OCSP endpoints are **HTTPS** (or org-approved distribution), highly available, independent of staff-iam uptime +- [ ] **R-03** CRL sign key / OCSP responder key custody documented (HSM preferred); not the IAM app key +- [ ] **R-04** CRL validity / nextUpdate cadence agreed (e.g. ≤ 24h) and OCSP cache skew understood +- [ ] **R-05** Reason codes mapped: `keyCompromise`, `cessationOfOperation`, `affiliationChanged` (offboard) +- [ ] **R-06** Short-lived leaf policy documented if org skips CRL for leaves shorter than cache TTL +- [ ] **R-07** Lab root / `lab-pki` **absent** from production trust + CRL fetch config + +--- + +## Proxy deploy (Option A) + +- [ ] **R-10** Production trust store = corporate root/intermediate only +- [ ] **R-11** Client cert verify **on** for staff doors (fail closed on `FAILED` / absent when enforce intended) +- [ ] **R-12** Revocation mode enabled: + - CRL file/URL periodic fetch, **or** + - OCSP (stapling where applicable; client-cert OCSP leaf check per proxy docs) +- [ ] **R-13** Soft-fail vs hard-fail policy explicit: + - **Hard-fail** recommended for staff mTLS when OCSP/CRL unreachable (document availability needs) + - Soft-fail only with compensating short lifetime + monitoring +- [ ] **R-14** Forward verify/DN headers to staff-iam per [ENROLL.md](../ENROLL.md) +- [ ] **R-15** Header spoofing blocked (proxy strips inbound client-supplied verify headers) +- [ ] **R-16** Document cache TTL and how to **force flush** after emergency revoke + +### nginx sketch (names only — adapt to platform) + +```nginx +# illustrative — not a copy-paste production profile +ssl_client_certificate /etc/ssl/staff-ca-chain.pem; +ssl_verify_client on; +# ssl_crl /etc/ssl/staff-ca.crl; # if CRL file mode +# OCSP: per nginx/OpenSSL build capabilities — configure in org template +proxy_set_header X-SSL-Client-Verify $ssl_client_verify; +proxy_set_header X-SSL-Client-DN $ssl_client_s_dn; +``` + +Exact header names must match what staff-iam already reads (see ENROLL.md). Do not invent production hostnames here. + +### Caddy / Envoy + +- Use org-standard mTLS + revoke modules. +- Same requirements: verify + revoke + forward identity headers + strip spoofed inbound. + +--- + +## CA-adjacent Signer (Option B add-on) + +- [ ] **R-20** `Signer.revoke({ serial, reason })` API live and audited +- [ ] **R-21** Revoke publishes to CRL and/or OCSP within agreed SLA (e.g. ≤ 5 minutes) +- [ ] **R-22** staff-iam (when live mint ships) dual-writes: Signer.revoke + metadata `revokedAt` + `mtlsEnrolled: false` +- [ ] **R-23** Bulk offboard job: all serials for principal → revoke → clear IAM flags +- [ ] **R-24** Monitoring: revoke latency, CRL publish failures, OCSP error rate + +--- + +## staff-iam deploy attestation + +- [ ] **R-30** No CRL/OCSP listen sockets on staff-iam service +- [ ] **R-31** `NODE_ENV=production`, `LAB_CA_ENABLE` unset +- [ ] **R-32** `GET /enroll/status` still reflects **proxy** verify — IAM does not claim crypto revoke itself +- [ ] **R-33** When metadata exists (future): status may show `serial` / `revokedAt` from store, still subordinate to proxy check +- [ ] **R-34** Admin UI never offers “Revoke” that only flips a boolean without CA action (if button ships, it must call Signer) + +--- + +## Offboard / emergency revoke runbook + +1. **Identify** user + device + cert serial (from CA inventory or future IAM metadata). +2. **Revoke at CA** (Option A portal/CLI or Option B `Signer.revoke`) with reason. +3. **IAM:** set `mtlsEnrolled: false`; optionally invalidate sessions. +4. **Flush or wait** proxy CRL/OCSP cache (R-16). +5. **Verify:** + - Present revoked cert → proxy verify fail / revoke fail + - `GET /enroll/status` without good cert → not `verifyOk` + - `STAFF_MTLS_ENFORCE=1` login path denies enrolled user without valid cert +6. **Ticket** serial, time, reason, actor (no PEM bodies in tickets if avoidable). + +Bulk tenant offboard: revoke all serials, then clear flags; do not rely on flag clear alone. + +--- + +## Lab vs production (do not confuse) + +| Item | Lab | Production | +|------|-----|------------| +| CRL helper | `./scripts/lab-test-ca.sh crl` → empty lab CRL | Real CA CRL with serials | +| Trust | dev proxy + lab-root only on lab machines | Corporate chain only | +| OCSP | none in staff-iam | CA/responder hosts | +| Mint | optional `LAB_CA_ENABLE=1` | external PKI or Signer; lab refused | +| Empty CRL | smoke config only | **Insufficient** as revoke proof | + +```sh +# LAB ONLY — never production trust +./scripts/lab-test-ca.sh init --dir ./lab-pki +./scripts/lab-test-ca.sh crl --dir ./lab-pki --out ./lab-pki/lab-empty.crl.pem +# point a *local* dev proxy at lab-root + empty CRL for config syntax checks only +``` + +--- + +## Acceptance tests (ops) + +- [ ] **T-01** Valid unrevoked client cert → proxy OK → IAM status shows proxy present +- [ ] **T-02** Unknown/untrusted cert → proxy fail → no spoofed verify header reaches IAM +- [ ] **T-03** Revoked serial after cache TTL → proxy fail +- [ ] **T-04** Emergency flush path works within agreed RTO +- [ ] **T-05** staff-iam production still returns **501** on `POST /enroll/csr` until live Signer cutover +- [ ] **T-06** `LAB_CA_ENABLE=1` on production binary still does not mint (server refuse) +- [ ] **T-07** Monitoring alerts on CRL fetch fail / OCSP unhealthy + +--- + +## Do not + +- [ ] Do **not** serve CRL/OCSP from staff-iam origin by default +- [ ] Do **not** load lab empty CRL in production +- [ ] Do **not** soft-fail revoke checks without short-lived certs + explicit risk acceptance +- [ ] Do **not** treat IAM enrollment flag as cryptographic revocation +- [ ] Do **not** enable lab CA to “work around” missing production CRL + +--- + +## Still left (ops / product) + +- Live CA CRL/OCSP hostnames and certs (out-of-band) +- Proxy config in org IaC +- Signer.revoke when model B/C mint ships +- Optional IAM metadata + status UI revoke state after live mint + +## Related + +- [CRL-OCSP-DESIGN.md](./CRL-OCSP-DESIGN.md) — architecture options A/B/C +- [PROD-CA-CUTOVER-RUNBOOK.md](./PROD-CA-CUTOVER-RUNBOOK.md) +- [AUTHZ-ENROLL-DESIGN.md](./AUTHZ-ENROLL-DESIGN.md) +- [PRODUCTION-PKI-REFUSE.md](./PRODUCTION-PKI-REFUSE.md) +- [PKI-MINT-BACKLOG.md](./PKI-MINT-BACKLOG.md) +- [TEST-CA-DESIGN.md](./TEST-CA-DESIGN.md) +- [ENROLL.md](../ENROLL.md) +- [`scripts/lab-test-ca.sh`](../scripts/lab-test-ca.sh) `crl` — LAB ONLY diff --git a/docs/CRL-OCSP-DESIGN.md b/docs/CRL-OCSP-DESIGN.md index 62c8ebd..be3f9f5 100644 --- a/docs/CRL-OCSP-DESIGN.md +++ b/docs/CRL-OCSP-DESIGN.md @@ -154,6 +154,9 @@ Bulk tenant/staff offboard: revoke **all** serials bound to that principal at th ## Related docs +- [CRL-OCSP-DEPLOY-CHECKLIST.md](./CRL-OCSP-DEPLOY-CHECKLIST.md) — **READY** production deploy checklist +- [PROD-CA-CUTOVER-RUNBOOK.md](./PROD-CA-CUTOVER-RUNBOOK.md) — **READY** CA cutover (includes revoke) +- [AUTHZ-ENROLL-DESIGN.md](./AUTHZ-ENROLL-DESIGN.md) — **READY** AuthZ revoke permissions - [PKI-MINT-BACKLOG.md](./PKI-MINT-BACKLOG.md) — live mint checklist; CRL/OCSP item - [TEST-CA-DESIGN.md](./TEST-CA-DESIGN.md) — lab CA init/sign; never production - [ENROLL.md](../ENROLL.md) — mTLS states, proxy headers, CSR scaffold diff --git a/docs/PKI-MINT-BACKLOG.md b/docs/PKI-MINT-BACKLOG.md index 7d26b61..74a5e0b 100644 --- a/docs/PKI-MINT-BACKLOG.md +++ b/docs/PKI-MINT-BACKLOG.md @@ -143,8 +143,23 @@ If product later needs in-app production mint/renew, that is a **new** design (r 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 diff --git a/docs/PROD-CA-CUTOVER-RUNBOOK.md b/docs/PROD-CA-CUTOVER-RUNBOOK.md new file mode 100644 index 0000000..a178a1a --- /dev/null +++ b/docs/PROD-CA-CUTOVER-RUNBOOK.md @@ -0,0 +1,183 @@ +# Production CA cutover runbook (READY package) + +**Status: READY package** — operator cutover checklist for live device-cert **mint / renew / revoke**. +**Not shipped in staff-iam code.** Default `POST /enroll/csr` stays **501**. Lab CA stays **default off** and is **refused** when `NODE_ENV=production`. + +This package tells ops how to cut over to a **real** production CA without using `scripts/lab-test-ca.sh` or `LAB_CA_ENABLE`. It does **not** implement a CA inside the IAM Node process. + +| Companion | Role | +|-----------|------| +| [PKI-MINT-BACKLOG.md](./PKI-MINT-BACKLOG.md) | Shipped surface + live-mint product checklist | +| [AUTHZ-ENROLL-DESIGN.md](./AUTHZ-ENROLL-DESIGN.md) | AuthN/AuthZ gates before dropping default 501 | +| [CRL-OCSP-DEPLOY-CHECKLIST.md](./CRL-OCSP-DEPLOY-CHECKLIST.md) | Revocation distribution at proxy/CA | +| [PRODUCTION-PKI-REFUSE.md](./PRODUCTION-PKI-REFUSE.md) | What stays refused under production defaults | +| [TEST-CA-DESIGN.md](./TEST-CA-DESIGN.md) | Lab fixture only — never production trust | +| [ENROLL.md](../ENROLL.md) | mTLS states, proxy headers, status contract | + +--- + +## Hard rules (do not violate on cutover) + +1. **LAB_CA_ENABLE default off** — never set in production images, systemd units, or k8s manifests. +2. **`NODE_ENV=production` refuses lab** even if someone sets `LAB_CA_ENABLE=1` (server gate). +3. **Do not trust `lab-pki/` roots** on staff fleets or production reverse proxies. +4. **Do not drop default 501** until a real `Signer` + AuthZ (see AuthZ package) pass go/no-go. +5. **Private keys never** enter the IAM user store, audit log, or enroll response as PKCS#12 passwords / leaf keys. +6. **Proxy remains TLS SoT** for “cert present on this request”; IAM `mtlsEnrolled` is enrollment policy only. + +--- + +## Choose production CA model (product decision — pick one) + +| Model | When to choose | staff-iam role | +|-------|----------------|----------------| +| **A. External corporate PKI** (ADCS, Venafi, smallstep, internal Intermediate) | Org already issues workstation client certs | Stay proxy-only; optional later `Signer` HTTP client | +| **B. EST / SCEP / ACME device profile** | Managed fleet enrollment at edge | Edge enrolls; IAM still proxy headers + flag | +| **C. HSM-backed delegated Signer service** | Product wants in-app `POST /enroll/csr` live mint | IAM calls Signer over mTLS; **never** holds CA key | +| **D. Keep external-only forever** | Lowest risk; current default | No code change; this runbook stops at proxy cutover | + +**Recommended first cutover:** Model **A** or **B** (no IAM crypto). Model **C** only after AuthZ package + Signer contract land. + +Document the choice out-of-band (ticket / security design). Do not invent CA hostnames in git. + +--- + +## Pre-flight (ops-owned CA + proxy) + +- [ ] **P-01** Issuing CA / intermediate chosen (model A–C above); HSM or offline root procedure documented offline +- [ ] **P-02** Leaf profile: clientAuth EKU, short lifetime (hours–days preferred), CN/SAN policy agreed with security +- [ ] **P-03** Reverse proxy terminates mTLS and verifies client chain against **production** trust store only +- [ ] **P-04** Proxy forwards enroll headers per [ENROLL.md](../ENROLL.md) (`ssl-client-verify`, DN, optional PEM presence — names as deployed) +- [ ] **P-05** CRL/OCSP path ready or short-lifetime-only policy accepted — [CRL-OCSP-DEPLOY-CHECKLIST.md](./CRL-OCSP-DEPLOY-CHECKLIST.md) +- [ ] **P-06** Mint / renew / revoke operator procedures exist at the CA (GUI, API, or runbook) **outside** staff-iam +- [ ] **P-07** Confirm production staff-iam env: `NODE_ENV=production`, **`LAB_CA_ENABLE` unset**, no `lab-pki` volume mounts +- [ ] **P-08** Confirm `GET /enroll/status` → `csrPostStatus: 501`, `labCa.enabled: false` on production host + +```sh +# production host smoke (expect scaffold — not a cert) +curl -sS https:///enroll/status | jq '{csrPostStatus, labCa}' +# → csrPostStatus: 501, labCa.enabled: false (or equivalent probe false) + +curl -sS -o /tmp/csr-body.json -w '%{http_code}\n' -X POST https:///enroll/csr \ + -H 'content-type: application/json' \ + -d '{"csr":"-----BEGIN CERTIFICATE REQUEST-----\nMIIB\n-----END CERTIFICATE REQUEST-----"}' +# → 501 (or 400 if PEM shape invalid) — never 201 under production defaults +``` + +--- + +## Cutover path A/B — external PKI (no IAM mint) + +Use when product keeps mint outside staff-iam (current shipped design). + +### Mint (issue workstation client cert) + +1. Operator or MDM enrolls device at corporate CA / EST / SCEP / ACME profile. +2. Install leaf + chain in OS/browser client-cert store (org procedure). +3. Browser hits staff door through mTLS-terminating proxy. +4. Confirm `GET /enroll/status`: + - `proxyHeadersSeen` / `mtlsPresent` / `proxy.verifyOk` as configured + - `state` progresses `not_enrolled` → `pending_proxy` → ready for admin flag +5. IAM admin: `PUT /users/:username` `{ "mtlsEnrolled": true }` +6. Only then consider `STAFF_MTLS_ENFORCE=1` on the door. + +### Renew + +1. Renew at CA **before** `notAfter` (org cadence; prefer automated short-lived renew). +2. Replace client cert in workstation store. +3. No staff-iam API required unless metadata binding ships later (backlog). +4. Spot-check `/enroll/status` still shows proxy verify after renew. + +### Revoke / offboard + +1. CA revoke serial (reason `keyCompromise` or `cessationOfOperation`). +2. IAM: `mtlsEnrolled: false` (+ kill sessions if policy requires). +3. Wait for proxy CRL/OCSP cache TTL or flush — [CRL-OCSP-DEPLOY-CHECKLIST.md](./CRL-OCSP-DEPLOY-CHECKLIST.md). +4. Verify: client no longer yields `proxy.verifyOk`; enforce path denies. + +**Do not** call `POST /enroll/csr` expecting a production cert — it returns **501**. + +--- + +## Cutover path C — delegated Signer (future live mint) + +Blocked until product implements real `Signer` + AuthZ. This section is the **go/no-go gate**, not permission to code. + +### Go criteria (all required) + +- [ ] **C-01** Signer service deployed (HSM or equivalent); mTLS client identity for staff-iam only +- [ ] **C-02** [AUTHZ-ENROLL-DESIGN.md](./AUTHZ-ENROLL-DESIGN.md) gates implemented and tested (session + role + rate limit + PoP) +- [ ] **C-03** Full CSR ASN.1 validation (not header-shape only); CN/SAN bind to IAM user — never trust client CN alone +- [ ] **C-04** Issue returns cert **chain PEM only** (no private key / PKCS#12 password in JSON) +- [ ] **C-05** Persist metadata only: serial, fingerprint, notAfter (no leaf private key) +- [ ] **C-06** Renew + revoke call Signer; offboard runbook dual-writes CA revoke + `mtlsEnrolled: false` +- [ ] **C-07** Default 501 removed **only** when Signer healthy; failure modes stay fail-closed (502/503), never lab CA +- [ ] **C-08** Tests: production env still refuses `LAB_CA_ENABLE`; no demo PEMs; audit never stores CSR PEM body +- [ ] **C-09** ENROLL.md / README / this runbook updated for the live path in the same release +- [ ] **C-10** Security sign-off on CA model + key custody + +### Signer contract sketch (names only) + +``` +Signer.issue({ csrPem, subjectUserId, deviceId? }) → { certificatePem, chainPem?, serial, notAfter } +Signer.renew({ serial | csrPem, subjectUserId }) → same as issue +Signer.revoke({ serial, reason }) → { revokedAt } +``` + +- Transport: mTLS or SPIFFE between staff-iam and Signer — **not** lab shell-out. +- staff-iam maps HTTP: + - `POST /enroll/csr` → `Signer.issue` (AuthZ’d) + - future renew/revoke routes → `Signer.renew` / `Signer.revoke` +- Lab path (`lab-test-ca.sh`) remains CI/dev fixture only; never selected when `NODE_ENV=production`. + +### Rollback + +1. Feature-flag or deploy previous staff-iam build that restores default **501**. +2. Leave external proxy mTLS + `mtlsEnrolled` path working (path A/B still valid). +3. Revoke any leaves issued during failed cutover at the CA. +4. **Do not** “fix” with `LAB_CA_ENABLE=1` in production. + +--- + +## Production env attestation (required every deploy) + +| Check | Expected | +|-------|----------| +| `NODE_ENV` | `production` | +| `LAB_CA_ENABLE` | **unset** / empty | +| `LAB_PKI_DIR` | unset; no lab-pki volume | +| `POST /enroll/csr` without live Signer | **501** `not_implemented` | +| `GET /enroll/status` `.labCa` | enabled/ready **false** | +| Trust store on proxy | production intermediate/root only | +| Image contents | no committed `lab-pki/` keys (gitignored) | + +--- + +## Do not + +- [ ] Do **not** enable `LAB_CA_ENABLE` on production, staging-shared, or customer-adjacent hosts +- [ ] Do **not** point production proxies at `lab-pki/lab-root.crt.pem` +- [ ] Do **not** drop 501 without C-01…C-10 +- [ ] Do **not** return PKCS#12 or private keys from staff-iam +- [ ] Do **not** treat `mtlsEnrolled: false` alone as cryptographic revoke (always CA revoke too) +- [ ] Do **not** invent CA URLs, RA credentials, or HSM slot IDs in this repository + +--- + +## Still left (ops / product — outside this READY package) + +- Deploy real CA or Signer (hostnames, credentials, HSM) +- Execute mint/renew/revoke on live serials +- Wire production proxy trust + headers +- Implement AuthZ + Signer client code (separate engineering change gated by C-01…C-10) +- Production CRL/OCSP endpoints — [CRL-OCSP-DEPLOY-CHECKLIST.md](./CRL-OCSP-DEPLOY-CHECKLIST.md) + +## Related + +- [PRODUCTION-PKI-REFUSE.md](./PRODUCTION-PKI-REFUSE.md) +- [PKI-MINT-BACKLOG.md](./PKI-MINT-BACKLOG.md) +- [AUTHZ-ENROLL-DESIGN.md](./AUTHZ-ENROLL-DESIGN.md) +- [CRL-OCSP-DEPLOY-CHECKLIST.md](./CRL-OCSP-DEPLOY-CHECKLIST.md) +- [TEST-CA-DESIGN.md](./TEST-CA-DESIGN.md) +- [ENROLL.md](../ENROLL.md) +- [`scripts/lab-test-ca.sh`](../scripts/lab-test-ca.sh) — LAB ONLY diff --git a/docs/PRODUCTION-PKI-REFUSE.md b/docs/PRODUCTION-PKI-REFUSE.md index 2515a91..1feede6 100644 --- a/docs/PRODUCTION-PKI-REFUSE.md +++ b/docs/PRODUCTION-PKI-REFUSE.md @@ -74,6 +74,9 @@ LAB_CA_ENABLE=1 npm start # never with NODE_ENV=production ## Related +- [PROD-CA-CUTOVER-RUNBOOK.md](./PROD-CA-CUTOVER-RUNBOOK.md) — **READY** prod CA cutover +- [AUTHZ-ENROLL-DESIGN.md](./AUTHZ-ENROLL-DESIGN.md) — **READY** AuthZ enroll design +- [CRL-OCSP-DEPLOY-CHECKLIST.md](./CRL-OCSP-DEPLOY-CHECKLIST.md) — **READY** CRL/OCSP deploy - [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 diff --git a/docs/TEST-CA-DESIGN.md b/docs/TEST-CA-DESIGN.md index 0e68728..3ea0bf9 100644 --- a/docs/TEST-CA-DESIGN.md +++ b/docs/TEST-CA-DESIGN.md @@ -190,3 +190,14 @@ Exact algorithms should match whatever production CA model is selected in [PKI-M - [ENROLL.md](../ENROLL.md) — mTLS states, proxy headers, CSR schema/scaffold - [README.md](../README.md) — service overview and security gates - [TOTP.md](../TOTP.md) — orthogonal authenticator 2FA (real crypto; unrelated to device-cert mint) + +## READY packages (production — not lab) + +Lab stays default-off / production-refused. External ops packages: + +- [PROD-CA-CUTOVER-RUNBOOK.md](./PROD-CA-CUTOVER-RUNBOOK.md) — production CA mint/renew/revoke +- [AUTHZ-ENROLL-DESIGN.md](./AUTHZ-ENROLL-DESIGN.md) — AuthZ before dropping 501 +- [CRL-OCSP-DEPLOY-CHECKLIST.md](./CRL-OCSP-DEPLOY-CHECKLIST.md) — production CRL/OCSP +- [PRODUCTION-PKI-REFUSE.md](./PRODUCTION-PKI-REFUSE.md) — refuse matrix +- [PKI-MINT-BACKLOG.md](./PKI-MINT-BACKLOG.md) — live mint checklist +