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
This commit is contained in:
George Lambert 2026-10-14 04:20:24 -04:00
commit 7e8fc7c1f9
9 changed files with 643 additions and 0 deletions

View file

@ -232,6 +232,13 @@ staff-iam **never** parses the PEM, checks expiry, or matches CN to username. Bi
## Related docs ## 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 - [TOTP.md](./TOTP.md) — authenticator setup, `STAFF_TOTP_REQUIRED`, login gate
- [README.md](./README.md) — install, usage, seed users - [README.md](./README.md) — install, usage, seed users
- [SUMMARY.md](./SUMMARY.md) — one-line service summary - [SUMMARY.md](./SUMMARY.md) — one-line service summary

View file

@ -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) | | [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/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/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 | | [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) | | [NATS.md](./NATS.md) | NATS notes (if used) |
| [SUMMARY.md](./SUMMARY.md) | One-line service summary | | [SUMMARY.md](./SUMMARY.md) | One-line service summary |

241
docs/AUTHZ-ENROLL-DESIGN.md Normal file
View file

@ -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)

View file

@ -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

View file

@ -154,6 +154,9 @@ Bulk tenant/staff offboard: revoke **all** serials bound to that principal at th
## Related docs ## 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 - [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 - [TEST-CA-DESIGN.md](./TEST-CA-DESIGN.md) — lab CA init/sign; never production
- [ENROLL.md](../ENROLL.md) — mTLS states, proxy headers, CSR scaffold - [ENROLL.md](../ENROLL.md) — mTLS states, proxy headers, CSR scaffold

View file

@ -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`. 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. 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 ## 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 - [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 - [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 - [CRL-OCSP-DESIGN.md](./CRL-OCSP-DESIGN.md) — future real-CA revocation (proxy CRL/OCSP); lab empty CRL only

View file

@ -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://<staff-iam-host>/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://<staff-iam-host>/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

View file

@ -74,6 +74,9 @@ LAB_CA_ENABLE=1 npm start # never with NODE_ENV=production
## Related ## 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 - [PKI-MINT-BACKLOG.md](./PKI-MINT-BACKLOG.md) — shipped surface + live checklist
- [TEST-CA-DESIGN.md](./TEST-CA-DESIGN.md) — lab script + gated HTTP - [TEST-CA-DESIGN.md](./TEST-CA-DESIGN.md) — lab script + gated HTTP
- [CRL-OCSP-DESIGN.md](./CRL-OCSP-DESIGN.md) — future revocation - [CRL-OCSP-DESIGN.md](./CRL-OCSP-DESIGN.md) — future revocation

View file

@ -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 - [ENROLL.md](../ENROLL.md) — mTLS states, proxy headers, CSR schema/scaffold
- [README.md](../README.md) — service overview and security gates - [README.md](../README.md) — service overview and security gates
- [TOTP.md](../TOTP.md) — orthogonal authenticator 2FA (real crypto; unrelated to device-cert mint) - [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