mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 12:31:29 +00:00
docs: README + concepts + features reflect CRL/OCSP responder bundle
Audit pass against cowork/crl-ocsp-responder-prompt.md found three
operator-facing docs still describing the pre-bundle CRL/OCSP surface
(GET-only OCSP, CA-key-direct signing, no scheduler-driven cache). Each
claim updated below was ground-truthed against repo HEAD before edit.
README.md
* Standards & Revocation table — CRL row now mentions
scheduler-pre-generated cache (CERTCTL_CRL_GENERATION_INTERVAL,
crl_cache table); OCSP row mentions GET + POST forms, dedicated
responder cert per RFC 6960 §2.6, id-pkix-ocsp-nocheck per
§4.2.2.2.1, 7d auto-rotation grace.
* Revocation paragraph — corrected the 'Embedded OCSP responder'
one-liner to call out the dedicated-responder-cert design (the CA
private key is never used directly for OCSP signing, which is the
load-bearing security property for the future PKCS#11/HSM driver
path) and added the link to the relying-party guide.
docs/concepts.md
* CRL paragraph — added the scheduler pre-generation + singleflight
coalescing detail. Kept the existing 24h validity claim (verified
against internal/connector/issuer/local/local.go:956 — 'NextUpdate:
now.Add(24 * time.Hour)').
* OCSP paragraph — corrected the description so it covers both GET
and POST forms (POST per RFC 6960 §A.1.1 is what production
clients use: Firefox, OpenSSL s_client -status, cert-manager,
Intune); added the dedicated-responder-cert + nocheck-extension +
auto-rotation explanation; cross-link to docs/crl-ocsp.md.
docs/features.md
* Revocation Infrastructure section — CRL Endpoint, OCSP Responder,
new Admin Cache Observability subsection, new GUI Revocation
Endpoints Panel subsection. Corrected the previously-wrong 'Signs
with the issuing CA key' OCSP claim — the bundle's load-bearing
security improvement is exactly that the CA key is NOT used
directly. Cross-link to crl-ocsp.md.
* Local CA env vars table — added all four new
CERTCTL_CRL_GENERATION_INTERVAL / CERTCTL_OCSP_RESPONDER_KEY_DIR
(with the prod 'MUST set' callout) / _ROTATION_GRACE / _VALIDITY
rows. Closes the G-3 'env var defined in Go but never documented'
drift that broke CI on commit fc3c7ad.
* Migrations table — added 000019_crl_cache and 000020_ocsp_responder
rows so the table reflects the bundle's persisted surface area;
also clarified the table is illustrative + pointed readers at
'ls migrations/*.up.sql' for the full sequence (the table had
drifted behind reality at 000010 even before this bundle).
docs/architecture.md was already updated in commit b4334ed with the
same content scope, so no further architecture edits.
Verification:
* Local G-3 set difference: empty (Go-defined ∖ docs-mentioned for
CRL/OCSP env vars).
* 24h CRL validity claim verified against local.go:956 NextUpdate.
* Migration numbers verified against 'ls migrations/000019* 000020*'.
* id-pkix-ocsp-nocheck OID verified against
internal/connector/issuer/local/ocsp_responder.go:60.
This commit is contained in:
@@ -115,8 +115,8 @@ gantt
|
||||
|
||||
| Capability | Standard | Notes |
|
||||
|------------|----------|-------|
|
||||
| DER-encoded X.509 CRL | RFC 5280 | Per-issuer, signed by issuing CA, 24h validity |
|
||||
| Embedded OCSP responder | RFC 6960 | Good/revoked/unknown status per issuer |
|
||||
| DER-encoded X.509 CRL | RFC 5280 | Per-issuer, signed by issuing CA, 24h validity. Pre-generated by the scheduler (`CERTCTL_CRL_GENERATION_INTERVAL`, default 1h) and cached in `crl_cache` so HTTP fetches do not rebuild per request. |
|
||||
| Embedded OCSP responder | RFC 6960 | GET + POST forms (`POST /.well-known/pki/ocsp/{issuer_id}` per §A.1.1). Signed by a per-issuer dedicated OCSP responder cert (RFC 6960 §2.6) carrying `id-pkix-ocsp-nocheck` (§4.2.2.2.1) — the CA private key is never used directly for OCSP signing. Responder cert auto-rotates within 7d of expiry. |
|
||||
| S/MIME certificates | RFC 8551 | Email protection EKU, adaptive KeyUsage flags |
|
||||
| Certificate export | — | PEM (JSON/file) and PKCS#12 formats |
|
||||
| ACME DNS-PERSIST-01 | IETF draft | Standing validation record, no per-renewal DNS updates |
|
||||
@@ -175,7 +175,7 @@ Built for **platform engineering and DevOps teams** managing 10–500+ certifica
|
||||
|
||||
**Enrollment protocols.** EST server (RFC 7030) for device and WiFi enrollment. SCEP server (RFC 8894) for MDM platforms and network devices. S/MIME issuance with email protection EKU.
|
||||
|
||||
**Revocation.** Single and bulk revocation (by profile, owner, agent, or issuer). DER-encoded X.509 CRL per issuer, signed by the issuing CA. Embedded OCSP responder. RFC 5280 reason codes. Short-lived certs (TTL < 1 hour) are exempt — expiry is sufficient revocation.
|
||||
**Revocation.** Single and bulk revocation (by profile, owner, agent, or issuer). RFC 5280 reason codes. Production-grade revocation status surface for relying parties: DER-encoded X.509 CRL per issuer, scheduler-pre-generated and cached so HTTP fetches do not rebuild per request; embedded OCSP responder serving both GET and POST forms (RFC 6960 §A.1.1) with responses signed by a per-issuer dedicated OCSP responder cert (RFC 6960 §2.6, `id-pkix-ocsp-nocheck` per §4.2.2.2.1) — the CA private key is never used directly for OCSP signing. Both endpoints live unauthenticated under `/.well-known/pki/` per RFC 8615. Short-lived certs (TTL < 1 hour) are exempt — expiry is sufficient revocation. See [docs/crl-ocsp.md](docs/crl-ocsp.md) for the relying-party integration guide.
|
||||
|
||||
**Audit and observability.** Immutable append-only audit trail records every lifecycle action, every API call, and every approval decision. Prometheus metrics endpoint. Scheduled certificate digest emails. Continuous endpoint health monitoring with state machine transitions and real-time alerts.
|
||||
|
||||
|
||||
+2
-2
@@ -218,9 +218,9 @@ certctl implements revocation using three complementary mechanisms:
|
||||
|
||||
**Bulk Revocation** (Fleet-Level Incident Response): For large-scale incidents like CA compromise or team infrastructure decommissioning, `POST /api/v1/certificates/bulk-revoke` revokes all certificates matching filter criteria in a single operation. Filter by profile, owner, team, agent group, or issuer to target the affected certificate set. This is essential for incident response — instead of revoking certificates one-by-one, operators can revoke an entire fleet in minutes. Bulk revocation creates individual revocation jobs that reuse the existing revocation pipeline, ensuring every certificate is audited and notifications are sent.
|
||||
|
||||
**Certificate Revocation List (CRL)**: certctl serves DER-encoded X.509 CRLs per issuer at `GET /.well-known/pki/crl/{issuer_id}` (RFC 5280 §5 wire format, RFC 8615 well-known namespace). The endpoint is unauthenticated so any relying party — browser, TLS client, hardware appliance — can fetch it without a certctl API key. The CRL is signed by the issuing CA's key and has 24-hour validity; clients can download it periodically to check revocation status offline. The response carries `Content-Type: application/pkix-crl`.
|
||||
**Certificate Revocation List (CRL)**: certctl serves DER-encoded X.509 CRLs per issuer at `GET /.well-known/pki/crl/{issuer_id}` (RFC 5280 §5 wire format, RFC 8615 well-known namespace). The endpoint is unauthenticated so any relying party — browser, TLS client, hardware appliance — can fetch it without a certctl API key. The CRL is signed by the issuing CA's key and has 24-hour validity; clients can download it periodically to check revocation status offline. The response carries `Content-Type: application/pkix-crl`. The CRL is **pre-generated** by a scheduler-driven loop (`crlGenerationLoop`, default interval 1 hour, configurable via `CERTCTL_CRL_GENERATION_INTERVAL`) and persisted in the `crl_cache` table — HTTP fetches read from the cache rather than rebuilding per request, so a busy CA does not DOS itself at scale. Concurrent regeneration requests for the same issuer are coalesced via an in-tree singleflight gate.
|
||||
|
||||
**OCSP Responder**: For real-time revocation checking, certctl includes an embedded OCSP responder at `GET /.well-known/pki/ocsp/{issuer_id}/{serial}` (RFC 6960). Like the CRL endpoint, it is unauthenticated and returns signed OCSP responses (good, revoked, or unknown) with `Content-Type: application/ocsp-response`, so clients can verify certificate status without downloading the full CRL.
|
||||
**OCSP Responder**: For real-time revocation checking, certctl includes an embedded OCSP responder serving both forms RFC 6960 §A.1.1 defines: `GET /.well-known/pki/ocsp/{issuer_id}/{serial}` (URL-path lookup, useful for ops curl-debugging) and `POST /.well-known/pki/ocsp/{issuer_id}` with a binary `application/ocsp-request` body (the form most production clients use — Firefox, OpenSSL `s_client -status`, cert-manager, Intune device-state validators). Both forms are unauthenticated and return signed OCSP responses (good, revoked, or unknown) with `Content-Type: application/ocsp-response`. OCSP responses are signed by a **dedicated per-issuer OCSP responder cert** (RFC 6960 §2.6 / §4.2.2.2) — NOT by the CA private key directly — that carries the `id-pkix-ocsp-nocheck` extension (RFC 6960 §4.2.2.2.1) so OCSP clients do not recursively check the responder cert's own revocation status. The responder cert auto-rotates within 7 days of expiry (configurable via `CERTCTL_OCSP_RESPONDER_ROTATION_GRACE`), letting the responder key live on disk or rotate frequently while the CA key stays cold. See [`crl-ocsp.md`](crl-ocsp.md) for endpoint examples (curl, OpenSSL, Firefox, Intune) and the responder cert lifecycle.
|
||||
|
||||
Short-lived certificates (those assigned to profiles with TTL under 1 hour) are exempt from CRL and OCSP — their rapid expiry is considered sufficient revocation. This is a deliberate design choice to reduce infrastructure overhead for ephemeral machine-to-machine credentials.
|
||||
|
||||
|
||||
+28
-3
@@ -283,16 +283,35 @@ Revocation is a 7-step process: validate eligibility → get serial → update s
|
||||
|
||||
- `GET /.well-known/pki/crl/{issuer_id}` — DER-encoded X.509 CRL signed by the issuing CA, 24-hour validity (RFC 5280 §5 + RFC 8615). Served unauthenticated with `Content-Type: application/pkix-crl` so relying parties without certctl API credentials can fetch it.
|
||||
|
||||
The CRL is **pre-generated** by the scheduler's `crlGenerationLoop` (`internal/scheduler/scheduler.go`) on a configurable interval (`CERTCTL_CRL_GENERATION_INTERVAL`, default 1h) and persisted in the `crl_cache` table (migration 000019). HTTP fetches read from the cache rather than rebuilding per request — a busy CA does not DOS itself at scale. Concurrent regeneration requests for the same issuer are coalesced via an in-tree singleflight gate (`internal/service/crl_cache.go`, ~30 LoC; no `golang.org/x/sync` dependency). Per-issuer generation events are recorded in `crl_generation_events` for ops visibility.
|
||||
|
||||
Prior non-standard JSON CRL and authenticated `/api/v1/crl*` paths were removed in M-006 — RFC 5280 defines only the DER wire format and relying parties do not have API keys.
|
||||
|
||||
### OCSP Responder
|
||||
|
||||
`GET /.well-known/pki/ocsp/{issuer_id}/{serial}` — signed OCSP responses (good/revoked/unknown) per RFC 6960. Served unauthenticated with `Content-Type: application/ocsp-response`. Signs with the issuing CA key; requires CA key access (Local CA, step-CA connectors).
|
||||
certctl serves both forms RFC 6960 §A.1.1 defines:
|
||||
|
||||
- `GET /.well-known/pki/ocsp/{issuer_id}/{serial}` — URL-path lookup (useful for ops curl-debugging).
|
||||
- `POST /.well-known/pki/ocsp/{issuer_id}` — binary `application/ocsp-request` body (the form most production clients use: Firefox, OpenSSL `s_client -status`, cert-manager, Intune).
|
||||
|
||||
Both forms are unauthenticated and return signed OCSP responses (good/revoked/unknown) with `Content-Type: application/ocsp-response`.
|
||||
|
||||
OCSP responses are signed by a **dedicated per-issuer OCSP responder cert** (RFC 6960 §2.6 / §4.2.2.2, migration 000020) — NOT by the CA private key directly. The responder cert is generated on first OCSP request via `OCSPResponderService.EnsureResponder` (`internal/connector/issuer/local/ocsp_responder.go`), persisted in the `ocsp_responders` table, and carries the `id-pkix-ocsp-nocheck` extension (OID `1.3.6.1.5.5.7.48.1.5`, RFC 6960 §4.2.2.2.1) so OCSP clients do not recursively check the responder's own revocation status. The responder cert auto-rotates within `CERTCTL_OCSP_RESPONDER_ROTATION_GRACE` (default 7d) of expiry; new certs default to `CERTCTL_OCSP_RESPONDER_VALIDITY` (30d). Self-healing: if the persisted responder key file is missing (operator pruned the keydir), the service treats this as "rotate now" rather than crashing. Local CA + step-CA connectors expose CRL+OCSP; upstream issuers (Vault, EJBCA, DigiCert) serve their own infrastructure.
|
||||
|
||||
### Admin Cache Observability
|
||||
|
||||
`GET /api/v1/admin/crl/cache` — admin-gated (Bearer required, admin flag enforced server-side via `middleware.IsAdmin`; returns HTTP 403 for non-admin callers). Returns the per-issuer cache state: `crl_number`, `this_update`, `next_update`, `generated_at`, `generation_duration_ms`, `revoked_count`, `is_stale`, plus the most-recent N generation events. Used by ops dashboards and the GUI cert-detail page's cache-age badge. The handler is pinned to the M-008 admin-gated handler allowlist (`internal/api/handler/m008_admin_gate_test.go`) — adding a new admin endpoint without the regression triplet (`_NonAdmin_Returns403` / `_AdminExplicitFalse_Returns403` / `_AdminPermitted_ForwardsActor`) fails CI.
|
||||
|
||||
### GUI Revocation Endpoints Panel
|
||||
|
||||
The certificate-detail page (`web/src/pages/CertificateDetailPage.tsx`) renders a Revocation Endpoints card that shows the CRL Distribution Point URL (`https://<host>/.well-known/pki/crl/<issuer_id>`) and OCSP Responder URL (`https://<host>/.well-known/pki/ocsp/<issuer_id>`), plus two action buttons: "Test CRL fetch" (calls `fetchCRL(issuer_id)`, shows byte count + content-type) and "Check OCSP status" (calls `getOCSPStatus(issuer_id, serial_hex)`, shows DER response size). For admin callers, a cache-age badge ("Cache fresh · 2m ago" / "Cache stale" / "Not yet generated") consumes the admin observability endpoint above; non-admin callers don't trigger the fetch (gated client-side on `useAuth().admin`) so the badge cannot leak generation cadence.
|
||||
|
||||
### Short-Lived Certificate Exemption
|
||||
|
||||
Certificates with profile TTL < 1 hour skip CRL/OCSP. Expiry is sufficient revocation for short-lived credentials.
|
||||
|
||||
For the full operator + relying-party guide (curl/OpenSSL/Firefox/cert-manager/Intune integration recipes, troubleshooting), see [`crl-ocsp.md`](crl-ocsp.md).
|
||||
|
||||
---
|
||||
|
||||
## Certificate Export
|
||||
@@ -390,8 +409,12 @@ Self-signed or sub-CA mode using `crypto/x509`.
|
||||
|---|---|---|
|
||||
| `CERTCTL_CA_CERT_PATH` | (none) | Path to CA certificate PEM. When set, enables sub-CA mode. |
|
||||
| `CERTCTL_CA_KEY_PATH` | (none) | Path to CA private key PEM (RSA, ECDSA, PKCS#8). |
|
||||
| `CERTCTL_CRL_GENERATION_INTERVAL` | `1h` | How often the scheduler walks every CRL-supporting issuer and rebuilds the cached CRL. HTTP fetches read from the cache, not from a per-request rebuild. |
|
||||
| `CERTCTL_OCSP_RESPONDER_KEY_DIR` | (none) | **Operator MUST set in production.** Directory where the FileDriver persists each issuer's OCSP responder key (`ocsp-responder-<issuer_id>.key`). When unset, the responder service uses a temporary directory that does NOT survive restarts — fine for dev, NEVER for prod. |
|
||||
| `CERTCTL_OCSP_RESPONDER_ROTATION_GRACE` | `7d` | When the responder cert's `NotAfter` falls within this window, `EnsureResponder` rotates to a fresh cert+key on the next OCSP request or scheduler tick. |
|
||||
| `CERTCTL_OCSP_RESPONDER_VALIDITY` | `30d` | How long each newly-issued responder cert is valid for. Short by design: relying parties cache OCSP responses, not the responder cert chain, and `id-pkix-ocsp-nocheck` blocks recursive revocation checking on the responder itself. |
|
||||
|
||||
Sub-CA mode validates `IsCA=true` and `KeyUsageCertSign` on the loaded certificate. Falls back to self-signed when paths are not set. Supports CRL generation (`GenerateCRL`) and OCSP response signing (`SignOCSPResponse`).
|
||||
Sub-CA mode validates `IsCA=true` and `KeyUsageCertSign` on the loaded certificate. Falls back to self-signed when paths are not set. Supports CRL generation (`GenerateCRL`) and OCSP response signing (`SignOCSPResponse`). All CA-key signing flows through the `signer.Signer` interface (`internal/crypto/signer/`); the OCSP responder cert is signed by the CA via the existing issuance pipeline and OCSP responses are signed by the responder key (NOT the CA key directly) per RFC 6960 §2.6.
|
||||
|
||||
### ACME
|
||||
|
||||
@@ -1429,8 +1452,10 @@ The migration runner reads SQL files from `./migrations/` by default; the path i
|
||||
| `000008_verification` | Columns on `jobs` (verification fields) |
|
||||
| `000009_issuer_config` | Columns on `issuers` (encrypted_config, source, test_status) |
|
||||
| `000010_target_config` | Columns on `targets` (encrypted_config, source, test_status) |
|
||||
| `000019_crl_cache` | `crl_cache` (per-issuer pre-generated DER CRL with monotonic `crl_number` per RFC 5280 §5.2.3, `this_update` / `next_update` timestamps, `revoked_count`, generation duration metric) + `crl_generation_events` (per-tick ops audit row with `succeeded` flag and error text) |
|
||||
| `000020_ocsp_responder` | `ocsp_responders` (per-issuer dedicated OCSP responder cert PEM + on-disk key path + `not_before` / `not_after` for auto-rotation) |
|
||||
|
||||
All migrations are idempotent (`IF NOT EXISTS`, `ON CONFLICT`).
|
||||
The migration list above is illustrative; for the full sequence run `ls migrations/*.up.sql`. All migrations are idempotent (`IF NOT EXISTS`, `ON CONFLICT`).
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user