Files
kca/ocsp-responder/README.md
T
woggioni aee1025015 Fix stale OCSP snapshots indefinitely
- Parse and validate  from snapshot JSON, reject future/missing timestamps
- Enforce snapshot age vs  in cache and stale fallback (no lifetime extension on repeat S3 reads)
- OCSP: , ,
- Remove unused  from signer
- Publisher heartbeats: , republish clean snapshots past interval, preserve dirty-only behavior and concurrency
- Config/env/docker/docs updates for heartbeat

Rolling upgrade: enable publisher heartbeats before deploying the age-enforcing responder so existing snapshots remain within the age window.
2026-10-26 12:06:15 +08:00

117 lines
4.4 KiB
Markdown

# KCA OCSP responder
Standalone Rust/Axum responder. It reads one full JSON status snapshot per CA
from S3-compatible object storage, caches it for one second with a single-flight
refresh, and signs OCSP responses with a delegated responder certificate.
It has **no KCA database access**, no Redis access, and no KCA master key. It
only needs:
- `s3:GetObject` on the configured bucket/prefix;
- the CA certificate and its KCA CA UUID for each issuer;
- a delegated OCSP signer certificate and private key for each CA. The
responder currently accepts RSA-SHA256 and ECDSA P-256 signers in PKCS#8,
PKCS#1 RSA, or SEC1 EC PEM form.
## Snapshot layout
KCA publishes objects as:
```
<prefix>/<KCA CA UUID>.json
```
Set each issuer's required `ca_id` to the UUID of its CA certificate row in KCA.
This isolates snapshots when the same public CA certificate exists in multiple
vaults. The responder validates both this ID and the CA certificate fingerprint
against every downloaded snapshot.
KCA publishes these objects when `KCA_OCSP_S3_BUCKET` is configured. The JSON
contains all certificates issued by that CA, both valid and revoked, and a
required `generated_at` timestamp. The responder never infers status from a
partial revoked-only list.
## Upgrading from fingerprint-named snapshots
Deploy the updated responder with `ca_id` before or together with the updated
KCA publisher. The responder deliberately does not fall back to legacy
`<prefix>/<CA certificate SHA-256>.json` objects. Until KCA publishes the new
objects, OCSP requests return `tryLater`. After verifying the new snapshots,
the obsolete fingerprint-named objects can be removed.
## Run locally
1. Create a config based on `responder.example.yaml`, setting each issuer's
`ca_id` to its CA certificate UUID in KCA.
2. Put your CA certificates and delegated signer files where the config points.
3. Export standard AWS credentials / endpoint variables as needed.
4. Make the signer key readable by the container user (`65532:65532` in the
published scratch image), or mount it via a Docker secret.
5. Run:
```sh
OCSP_CONFIG=./responder.yaml cargo run
```
The default listen address is `0.0.0.0:2560`.
The published image is built by Gitea Actions on `release/*` tags:
```sh
docker run --rm -p 2560:2560 \
-v "$PWD/responder.yaml:/etc/ocsp/responder.yaml:ro" \
-v "$PWD/ca:/etc/ocsp/ca:ro" \
-v "$PWD/signers:/etc/ocsp/signers:ro" \
-e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY \
gitea.woggioni.net/woggioni/kca-ocsp:latest
```
It runs `FROM scratch` and contains only a statically linked musl executable
and a CA bundle for outbound S3 HTTPS. There is no shell or package manager in
the image.
## Verify with OpenSSL
For a CA certificate `ca.crt` and leaf certificate `leaf.crt`:
```sh
openssl ocsp \
-issuer ca.crt \
-cert leaf.crt \
-url http://127.0.0.1:2560/ocsp \
-resp_text \
-noverify
```
`-noverify` skips local trust setup; the response still shows the signature
algorithm and included responder certificate. For production verification,
build a trust store containing the CA or validate with your normal PKI tooling.
## Endpoints
- `POST /ocsp` — `application/ocsp-request`
- `GET /ocsp/<base64url-request>` — optional GET form
- `GET /healthz`
- `GET /readyz`
## Cache and availability
- Per-CA cache TTL: `s3.cache_ttl_ms` (default 1000 ms).
- Concurrent requests for the same CA share one S3 `GetObject`.
- `s3.stale_grace_seconds` is also the hard maximum snapshot age, measured from
`generated_at`. Expired or future-dated snapshots are rejected even when S3
remains readable; if no valid cached snapshot remains, OCSP returns
`tryLater`.
- If a refresh fails, a cached snapshot is served only while both its time since
the last successful fetch and its `generated_at` age remain within
`s3.stale_grace_seconds`.
- KCA republishes unchanged snapshots every
`KCA_OCSP_HEARTBEAT_INTERVAL_SECONDS` (default 30 seconds). Keep that interval
comfortably below the responder's `s3.stale_grace_seconds` (default 60). For
a rolling upgrade, enable publisher heartbeats before the freshness-enforcing
responder so existing snapshots remain within the age limit.
- OCSP `thisUpdate` is the snapshot's `generated_at`. `nextUpdate` is capped at
the snapshot's hard expiry, while `producedAt` is the response signing time.
- With multiple CAs, the TTL is per CA. If you need a global one-request-per-
second limit across all CAs, that requires a shared rate limiter.