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
9.2 KiB
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 | Ops cutover + Signer go/no-go |
| PKI-MINT-BACKLOG.md | Live mint checklist |
| PRODUCTION-PKI-REFUSE.md | Refuse matrix |
| ENROLL.md | Current HTTP contract |
| 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
- Self mint: path subject = session user id; ignore client-supplied user id fields.
- Admin mint: body may include
username/userIdtarget; requirestaff.enroll.admin; audit both actor and target. - CN / SAN: derived server-side from IAM username / email / device record — client
commonNameis a hint only (same as lab today) and must match policy or be overwritten. - 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):
{
"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):
{
"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
Until then keep:
{ "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
- Keep scaffold 501 + lab refuse (done).
- Land Signer interface + feature flag off.
- AuthN/AuthZ + rate limit behind flag (still 501 when flag off).
- CSR ASN.1 validation behind flag.
- Metadata + status fields.
- Soak with flag on in lab/staging against real Signer (not lab-test-ca as production stand-in).
- Production cutover per runbook; remove default 501 only when flag on + Signer healthy.
- 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/catalogalone - Do not trust client CN/SAN without server policy
- Do not log CSR/cert PEM
- Do not default
STAFF_CSR_MINTon before soak - Do not return 201 without AuthZ once live path ships