verae-staff-iam/docs/AUTHZ-ENROLL-DESIGN.md
George Lambert 7e8fc7c1f9 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
2026-10-14 04:20:24 -04:00

9.2 KiB
Raw Permalink Blame History

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

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

{
  "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

  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