docs: CRL-OCSP-DESIGN + lab-test-ca crl subcommand
Future real-CA revocation design; lab empty CRL generator only.
This commit is contained in:
parent
ca117eb458
commit
a973431935
4 changed files with 250 additions and 8 deletions
160
docs/CRL-OCSP-DESIGN.md
Normal file
160
docs/CRL-OCSP-DESIGN.md
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
# CRL / OCSP design (future real CA)
|
||||
|
||||
**Status:** design only — **not implemented** in staff-iam.
|
||||
|
||||
- No CRL distribution endpoint
|
||||
- No OCSP responder
|
||||
- No revocation API on enroll/mint paths
|
||||
- Lab helper may emit an **empty** CRL file for local proxy experiments only ([`scripts/lab-test-ca.sh`](../scripts/lab-test-ca.sh) `crl`)
|
||||
|
||||
Production device-cert **mint** is still out of scope ([PKI-MINT-BACKLOG.md](./PKI-MINT-BACKLOG.md)). Lab mint (`LAB_CA_ENABLE=1`) is never a production CA and must not be treated as a revocation authority.
|
||||
|
||||
Canonical enroll contract: [ENROLL.md](../ENROLL.md). Lab CA: [TEST-CA-DESIGN.md](./TEST-CA-DESIGN.md). Platform revocation baseline: short lifetimes first; CRL/OCSP for intermediates and offboard bulk revoke (security-reference §6.4).
|
||||
|
||||
## Purpose
|
||||
|
||||
When (and only when) product ships a **real** production `Signer` for staff device certificates, operators need a documented revocation story:
|
||||
|
||||
1. How a lost/stolen workstation cert is invalidated before natural expiry.
|
||||
2. How reverse proxies and mTLS terminators learn about revocations.
|
||||
3. What staff-iam stores (metadata only) vs what the CA / edge owns.
|
||||
4. What stays **out** of the IAM Node process (no in-process OCSP, no fake “revoked” demo PEMs).
|
||||
|
||||
This document is that story. It does **not** authorize implementing CRL/OCSP inside staff-iam today.
|
||||
|
||||
## Non-goals (now)
|
||||
|
||||
| Non-goal | Why |
|
||||
|----------|-----|
|
||||
| Ship CRL or OCSP HTTP from staff-iam | IAM is not a CA; default mint stays **501** |
|
||||
| Soften mTLS with a stub “revoked” flag that is not backed by CA state | Violates “no fake crypto” |
|
||||
| Require CRL checks for lab-only leaves | Lab leaves are short-lived (≤30d script cap; default 7d); throwaway trust |
|
||||
| Put private CA keys or OCSP signing keys in the IAM user store | Keys stay in HSM / external PKI / `LAB_PKI_DIR` (lab only) |
|
||||
| Browser “Revoke cert” that only flips `mtlsEnrolled` | Admin flag is enrollment policy, not cryptographic revocation |
|
||||
|
||||
## Recommended production model
|
||||
|
||||
Align with Verae security-reference provisioning:
|
||||
|
||||
| Control | Role |
|
||||
|---------|------|
|
||||
| **Short leaf lifetime** | Primary control (hours–days for staff device certs; renew via real mint path) |
|
||||
| **CRL and/or OCSP on issuing intermediate** | Secondary control for early revoke and offboard |
|
||||
| **Proxy / edge termination** | Performs chain verify **and** revocation check; forwards verify/DN headers to IAM |
|
||||
| **staff-iam** | Stores enrollment flag + optional cert **metadata** (serial, fingerprint, notAfter); never signs CRL/OCSP itself unless product later splits a dedicated PKI service |
|
||||
|
||||
```
|
||||
Corporate / HSM CA (or EST/SCEP/ACME delegate)
|
||||
│ signs leaves
|
||||
│ publishes CRL URI and/or OCSP URL in AIA/CRLDP
|
||||
▼
|
||||
Reverse proxy (mTLS terminate)
|
||||
│ verify + CRL/OCSP (or short-lived-only policy)
|
||||
│ forward ssl-client-verify / DN / optional PEM presence
|
||||
▼
|
||||
staff-iam GET /enroll/status · STAFF_MTLS_ENFORCE
|
||||
│ mtlsEnrolled flag + future serial metadata
|
||||
▼
|
||||
Login / My security UI
|
||||
```
|
||||
|
||||
**Preferred default for staff workstations:** short-lived client certs + automated renew, with OCSP stapling or CRL fetch at the **proxy**, not inside the IAM app.
|
||||
|
||||
## Options (choose at live-mint design time)
|
||||
|
||||
### A — Proxy-side only (recommended first)
|
||||
|
||||
- Issuing CA publishes CRL Distribution Points and/or OCSP in leaf/intermediate extensions.
|
||||
- nginx / Caddy / Envoy / corporate WAF enables client-cert verify + revocation.
|
||||
- staff-iam continues to trust `proxy.verifyOk` / DN headers only.
|
||||
- Offboard: admin sets `mtlsEnrolled: false` **and** CA revokes serial (runbook both).
|
||||
|
||||
**Pros:** no new IAM crypto surface; matches today’s header contract.
|
||||
**Cons:** revoke latency depends on CRL TTL / OCSP cache; operators must wire proxy correctly.
|
||||
|
||||
### B — CA-adjacent PKI service (not in IAM process)
|
||||
|
||||
- Dedicated signer service owns issue + revoke + CRL/OCSP endpoints.
|
||||
- staff-iam calls `Signer.issue` / `Signer.revoke(serial)` over mTLS; never holds CA key.
|
||||
- Status UI may show “revoked at CA” from metadata poll, not by serving CRL bytes.
|
||||
|
||||
**Pros:** clean AuthZ boundary; scalable OCSP.
|
||||
**Cons:** new service + ops; still need proxy to enforce at TLS.
|
||||
|
||||
### C — In-process staff-iam OCSP/CRL (discouraged)
|
||||
|
||||
- Only if product explicitly requires IAM to be the issuing CA (unlikely).
|
||||
- Would need HSM, AIA URLs, cache, audit, rate limits, and production hardening.
|
||||
- **Do not** start here; backlog assumes external/HSM/EST signer first.
|
||||
|
||||
## Data staff-iam may hold later (metadata only)
|
||||
|
||||
When live mint ships, persist **at most**:
|
||||
|
||||
| Field | Use |
|
||||
|-------|-----|
|
||||
| `serial` (hex) | Correlate revoke + support |
|
||||
| `fingerprint` (SHA-256 of DER) | Display / audit |
|
||||
| `notAfter` | Renew warnings |
|
||||
| `revokedAt` / `revokeReason` | UI + audit after CA confirms |
|
||||
| `deviceId` (optional) | Bind to workstation record |
|
||||
|
||||
Never store leaf private keys, PKCS#12 passwords, or full cert PEM in the user JSON unless a separate encrypted secret store is designed. Audit events may log serial/fingerprint flags only (same rule as today’s `csr_attempt` — no PEM bodies).
|
||||
|
||||
## Revoke / offboard runbook (future)
|
||||
|
||||
1. Confirm identity (staff user + device).
|
||||
2. Call CA revoke (CRL entry and/or OCSP revoked status) with reason (`keyCompromise` / `cessationOfOperation`).
|
||||
3. Set `mtlsEnrolled: false` (and any device record) in IAM.
|
||||
4. Optionally kill active sessions (orthogonal to cert crypto).
|
||||
5. Wait for proxy CRL/OCSP cache TTL or force cache flush.
|
||||
6. Verify: client cert no longer yields `proxy.verifyOk`; login enforce path denies.
|
||||
|
||||
Bulk tenant/staff offboard: revoke **all** serials bound to that principal at the CA, then clear IAM flags.
|
||||
|
||||
## Lab only — empty CRL helper
|
||||
|
||||
[`scripts/lab-test-ca.sh`](../scripts/lab-test-ca.sh) optional subcommand:
|
||||
|
||||
```sh
|
||||
./scripts/lab-test-ca.sh crl [--dir DIR] [--out PATH]
|
||||
```
|
||||
|
||||
- Requires an initialized lab CA (`init`).
|
||||
- Writes a **valid, empty** CRL (no revoked serials) signed by the lab root, default `DIR/lab-empty.crl.pem`.
|
||||
- Purpose: let a **local** dev proxy load a CRL file path so verify+CRL config can be smoke-tested without implying production revocation.
|
||||
- **NOT FOR PRODUCTION.** Empty CRL does not revoke anything; lab root must not be in real trust stores.
|
||||
- Does **not** add HTTP CRL/OCSP routes to staff-iam.
|
||||
|
||||
## HTTP surfaces (not shipped)
|
||||
|
||||
| Surface | Today | Future (product decision) |
|
||||
|---------|--------|---------------------------|
|
||||
| `POST /enroll/csr` | 501 default; lab 201 only | Real sign; still no CRL body |
|
||||
| `POST /enroll/revoke` (name TBD) | absent | Optional: AuthZ’d request that calls external `Signer.revoke` |
|
||||
| `GET /crl/...` or OCSP | absent | Prefer CA/proxy hostnames in cert AIA/CRLDP — not staff-iam origin |
|
||||
| `GET /enroll/status` | proxy headers + flags | May add serial / notAfter / revokedAt metadata |
|
||||
|
||||
## Implementation order (blocked on product)
|
||||
|
||||
1. Choose production CA model in [PKI-MINT-BACKLOG.md](./PKI-MINT-BACKLOG.md) (external ACME/EST/HSM/delegated).
|
||||
2. Decide Option A vs B for revocation enforcement (default **A**).
|
||||
3. Document proxy verify+CRL/OCSP settings in operator ENROLL runbook.
|
||||
4. Add `Signer.revoke` + IAM metadata fields when mint is real.
|
||||
5. Only then consider bulk offboard automation and status UI revoke state.
|
||||
6. **Never** enable lab empty-CRL or lab root as a production trust anchor.
|
||||
|
||||
## Hard rules
|
||||
|
||||
1. **No fake revocation** — do not return success on a revoke API that only flips a boolean without CA action.
|
||||
2. **Proxy remains TLS source of truth** for “cert valid on this request.”
|
||||
3. **Lab CRL is empty + lab-signed only** — experiments, not fleet security.
|
||||
4. **Short lifetime remains the primary control**; CRL/OCSP are complementary.
|
||||
5. Cross-link live-mint PRs: this doc + [PKI-MINT-BACKLOG.md](./PKI-MINT-BACKLOG.md) + [ENROLL.md](../ENROLL.md) + [TEST-CA-DESIGN.md](./TEST-CA-DESIGN.md).
|
||||
|
||||
## Related docs
|
||||
|
||||
- [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
|
||||
- [README.md](../README.md) — service overview and security gates
|
||||
|
|
@ -79,10 +79,10 @@ Still left for live PKI: full checklist under “Left for live PKI” below.
|
|||
|
||||
Shipped for **lab/dev only** (loud warnings; never production):
|
||||
|
||||
- [x] [`scripts/lab-test-ca.sh`](../scripts/lab-test-ca.sh) — OpenSSL self-signed lab root (`init`) + sign CSR → leaf PEM/chain (`sign`); loud **NOT FOR PRODUCTION** warnings; default output `lab-pki/` (gitignored)
|
||||
- [x] [`scripts/lab-test-ca.sh`](../scripts/lab-test-ca.sh) — OpenSSL self-signed lab root (`init`) + sign CSR → leaf PEM/chain (`sign`); optional empty **`crl`**; loud **NOT FOR PRODUCTION** warnings; default output `lab-pki/` (gitignored)
|
||||
- [x] **Opt-in HTTP:** `LAB_CA_ENABLE=1` + lab-pki ready → `POST /enroll/csr` shells out to `sign` → **201** + `certificate` PEM + `X-Lab-CA: 1`
|
||||
- [x] **Default off** → HTTP path stays **501**; `NODE_ENV=production` refuses lab path
|
||||
- [x] Documented in [ENROLL.md](../ENROLL.md) + [TEST-CA-DESIGN.md](./TEST-CA-DESIGN.md)
|
||||
- [x] Documented in [ENROLL.md](../ENROLL.md) + [TEST-CA-DESIGN.md](./TEST-CA-DESIGN.md) + [CRL-OCSP-DESIGN.md](./CRL-OCSP-DESIGN.md)
|
||||
|
||||
| Layer | Lab path | Production mint (backlog below) |
|
||||
|-------|----------|----------------------------------|
|
||||
|
|
@ -129,7 +129,7 @@ Checklist for a real in-app mint (product + security design required before codi
|
|||
- [ ] Persist serial / fingerprint / notAfter for status and renew gates (store metadata only)
|
||||
- [ ] `GET /enroll/status` (or successor) surfaces real cert metadata when present
|
||||
- [ ] Renew / re-key endpoint and expiry warnings; revoke on offboard
|
||||
- [ ] CRL/OCSP or proxy-side revocation story documented
|
||||
- [x] ~~CRL/OCSP or proxy-side revocation story documented~~ — [CRL-OCSP-DESIGN.md](./CRL-OCSP-DESIGN.md); lab empty CRL via `lab-test-ca.sh crl` (not production)
|
||||
- [ ] Remove 501 scaffold; keep “no fake crypto” rule for tests (fixtures may use offline [`scripts/lab-test-ca.sh`](../scripts/lab-test-ca.sh) or a future gated test CA — see [TEST-CA-DESIGN.md](./TEST-CA-DESIGN.md); **never** default-on in server)
|
||||
- [ ] Operator runbooks + ENROLL.md / README updated for the live path
|
||||
|
||||
|
|
@ -146,7 +146,8 @@ If product later needs in-app production mint/renew, that is a **new** design (r
|
|||
## Related docs
|
||||
|
||||
- [TEST-CA-DESIGN.md](./TEST-CA-DESIGN.md) — lab script (offline) + **future** gated test CA design; no HTTP fake mint
|
||||
- [`scripts/lab-test-ca.sh`](../scripts/lab-test-ca.sh) — OpenSSL lab CA init/sign (NOT production; optional `LAB_CA_ENABLE=1` HTTP shell-out)
|
||||
- [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)
|
||||
- [ENROLL.md](../ENROLL.md) — mTLS states, `nextSteps`, `proxyHeadersSeen`, proxy header contract, admin workflow
|
||||
- [README.md](../README.md) — install, usage, staff security gates (mTLS row: proxy headers only, no device-cert mint)
|
||||
- [TOTP.md](../TOTP.md) — authenticator 2FA (orthogonal gate)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ Live mint product backlog and hard “no fake crypto” rules: [PKI-MINT-BACKLOG
|
|||
| Item | Detail |
|
||||
|------|--------|
|
||||
| Path | [`scripts/lab-test-ca.sh`](../scripts/lab-test-ca.sh) |
|
||||
| Offline | `init` → self-signed lab root under `lab-pki/` (gitignored); `sign --csr` → short-lived leaf PEM + chain on disk |
|
||||
| Offline | `init` → self-signed lab root under `lab-pki/` (gitignored); `sign --csr` → short-lived leaf PEM + chain on disk; optional `crl` → empty lab CRL |
|
||||
| Opt-in HTTP | `LAB_CA_ENABLE=1` + lab-pki ready → staff-iam shells out to `sign` on `POST /enroll/csr` → **201** + `X-Lab-CA: 1` |
|
||||
| Default | Flag **off** → `POST /enroll/csr` **501** (unchanged production scaffold) |
|
||||
| Production refuse | `NODE_ENV=production` → lab path off even if `LAB_CA_ENABLE=1` |
|
||||
|
|
@ -92,7 +92,7 @@ Proxy trusts corp roots Dev proxy may trust lab-pki root (lab only)
|
|||
```
|
||||
platform/verae-staff-iam/
|
||||
scripts/
|
||||
lab-test-ca.sh # LAB ONLY: init root + sign CSR → PEM on disk
|
||||
lab-test-ca.sh # LAB ONLY: init root + sign CSR + empty crl → PEM on disk
|
||||
lab-pki/ # default output dir (gitignored; keys never committed)
|
||||
```
|
||||
|
||||
|
|
@ -186,6 +186,7 @@ Exact algorithms should match whatever production CA model is selected in [PKI-M
|
|||
## Related docs
|
||||
|
||||
- [PKI-MINT-BACKLOG.md](./PKI-MINT-BACKLOG.md) — shipped proxy/schema/501 surface; live mint checklist; no-fake-crypto rules
|
||||
- [CRL-OCSP-DESIGN.md](./CRL-OCSP-DESIGN.md) — future production CRL/OCSP; lab empty CRL helper
|
||||
- [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)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
# Usage:
|
||||
# scripts/lab-test-ca.sh init [--dir DIR]
|
||||
# scripts/lab-test-ca.sh sign --csr PATH [--dir DIR] [--out PATH] [--days N] [--cn CN]
|
||||
# scripts/lab-test-ca.sh crl [--dir DIR] [--out PATH]
|
||||
# scripts/lab-test-ca.sh warn
|
||||
#
|
||||
# Defaults: DIR=./lab-pki (gitignored). Requires openssl on PATH.
|
||||
|
|
@ -53,13 +54,18 @@ Usage:
|
|||
--days leaf notAfter (default ${DAYS_DEFAULT}, max 30 for lab)
|
||||
--cn optional subject CN override in the issued cert (lab only)
|
||||
|
||||
$SCRIPT_NAME crl [--dir DIR] [--out PATH]
|
||||
Write a lab-only **empty** CRL (no revoked serials) signed by the lab root.
|
||||
--out defaults to DIR/lab-empty.crl.pem
|
||||
For local proxy CRL-path smoke tests only — NOT production revocation.
|
||||
|
||||
$SCRIPT_NAME warn
|
||||
Print the lab-only warning and exit 0.
|
||||
|
||||
Environment:
|
||||
LAB_PKI_DIR Override default DIR when --dir omitted.
|
||||
|
||||
Exit codes: 0 ok · 1 usage/input · 2 missing openssl/CA · 3 sign failure
|
||||
Exit codes: 0 ok · 1 usage/input · 2 missing openssl/CA · 3 sign/CRL failure
|
||||
EOF
|
||||
}
|
||||
|
||||
|
|
@ -139,7 +145,7 @@ Root cert: lab-root.crt.pem (public)
|
|||
Root key: lab-root.key.pem (PRIVATE — never commit, never production trust)
|
||||
With LAB_CA_ENABLE=1 the staff-iam server may shell-out sign against this dir.
|
||||
Default (flag off) POST /enroll/csr stays 501. NEVER production.
|
||||
See docs/TEST-CA-DESIGN.md, ENROLL.md, docs/PKI-MINT-BACKLOG.md.
|
||||
See docs/TEST-CA-DESIGN.md, docs/CRL-OCSP-DESIGN.md, ENROLL.md, docs/PKI-MINT-BACKLOG.md.
|
||||
EOF
|
||||
|
||||
echo "lab CA created:"
|
||||
|
|
@ -254,6 +260,79 @@ EOF
|
|||
warn_banner
|
||||
}
|
||||
|
||||
cmd_crl() {
|
||||
local dir="" out=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--dir) dir="${2:-}"; shift 2 ;;
|
||||
--out) out="${2:-}"; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "error: unknown crl arg: $1" >&2; usage >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
need_openssl
|
||||
warn_banner
|
||||
dir="$(resolve_dir "$dir")"
|
||||
local key="$dir/lab-root.key.pem"
|
||||
local ca="$dir/lab-root.crt.pem"
|
||||
|
||||
if [[ ! -f "$key" || ! -f "$ca" ]]; then
|
||||
echo "error: lab CA missing under $dir — run: $SCRIPT_NAME init --dir $dir" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ -z "$out" ]]; then
|
||||
out="$dir/lab-empty.crl.pem"
|
||||
fi
|
||||
|
||||
# Empty CRL: no revoked serials. Lab smoke-test fixture only.
|
||||
local crl_cnf="$dir/lab-crl.cnf"
|
||||
local crl_idx="$dir/lab-crl-index.txt"
|
||||
local crl_attr="$dir/lab-crl-index.txt.attr"
|
||||
: >"$crl_idx"
|
||||
echo "unique_subject = no" >"$crl_attr"
|
||||
cat >"$crl_cnf" <<EOF
|
||||
[ ca ]
|
||||
default_ca = lab_ca
|
||||
|
||||
[ lab_ca ]
|
||||
dir = $dir
|
||||
database = $crl_idx
|
||||
serial = $dir/lab-root.srl
|
||||
certificate = $ca
|
||||
private_key = $key
|
||||
default_md = sha256
|
||||
default_days = 30
|
||||
default_crl_days = 30
|
||||
crlnumber = $dir/lab-crlnumber
|
||||
copy_extensions = none
|
||||
policy = policy_any
|
||||
|
||||
[ policy_any ]
|
||||
commonName = optional
|
||||
EOF
|
||||
if [[ ! -f "$dir/lab-crlnumber" ]]; then
|
||||
echo "01" >"$dir/lab-crlnumber"
|
||||
fi
|
||||
|
||||
if ! openssl ca -gencrl -config "$crl_cnf" -out "$out" 2>"$dir/last-crl.err"; then
|
||||
echo "error: openssl gencrl failed:" >&2
|
||||
cat "$dir/last-crl.err" >&2 || true
|
||||
exit 3
|
||||
fi
|
||||
rm -f "$dir/last-crl.err"
|
||||
|
||||
echo "lab empty CRL written (NOT FOR PRODUCTION):"
|
||||
echo " crl: $out"
|
||||
echo " revoked entries: 0 (empty)"
|
||||
openssl crl -in "$out" -noout -text 2>/dev/null | head -n 20 || true
|
||||
echo
|
||||
echo "Dev proxy only: point CRL path at this file if testing verify+CRL config."
|
||||
echo "See docs/CRL-OCSP-DESIGN.md — empty lab CRL, never production revocation."
|
||||
warn_banner
|
||||
}
|
||||
|
||||
main() {
|
||||
local cmd="${1:-}"
|
||||
if [[ -z "$cmd" ]]; then
|
||||
|
|
@ -265,6 +344,7 @@ main() {
|
|||
case "$cmd" in
|
||||
init) cmd_init "$@" ;;
|
||||
sign) cmd_sign "$@" ;;
|
||||
crl) cmd_crl "$@" ;;
|
||||
warn) warn_banner; exit 0 ;;
|
||||
-h|--help|help) usage; exit 0 ;;
|
||||
*)
|
||||
|
|
|
|||
Loading…
Reference in a new issue