120 lines
4.7 KiB
Markdown
120 lines
4.7 KiB
Markdown
# KCA OCSP responder
|
|
|
|
Standalone Rust/Axum responder. It reads one full CBOR status snapshot per CA
|
|
from Redis, 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** and no KCA master key. It only needs:
|
|
|
|
- read access to OCSP keys in the Redis instance selected by KCA (the session
|
|
Redis by default, or a dedicated instance when `KCA_OCSP_REDIS_URL` is set);
|
|
- 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.
|
|
|
|
Where Redis ACLs are available, restrict the responder's credentials to the
|
|
OCSP prefix so it cannot read session entries. When sharing an instance, KCA
|
|
needs access to both `ocsp:*` and `kaya:session:*`; a dedicated OCSP instance
|
|
can use separate credentials.
|
|
|
|
## Snapshot layout
|
|
|
|
KCA publishes keys as:
|
|
|
|
```
|
|
<prefix>:<KCA CA UUID>
|
|
```
|
|
|
|
The default prefix is `ocsp`. 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 keys when either `REDIS_URL` or `KCA_OCSP_REDIS_URL` is
|
|
configured, preferring the dedicated OCSP URL when both are present. The
|
|
`kca-ocsp-status/v2` payload is encoded with CBOR and contains all certificates
|
|
issued by that CA, both valid and revoked, without a generation timestamp.
|
|
Freshness is carried by the Redis key TTL. The responder never infers status
|
|
from a partial revoked-only list. Publisher and responder upgrades must be
|
|
coordinated because legacy JSON payloads are rejected until republished.
|
|
|
|
## 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. Set `redis.url` or `OCSP_REDIS_URL` to the OCSP Redis URL used by KCA (the
|
|
session `REDIS_URL` unless `KCA_OCSP_REDIS_URL` overrides it).
|
|
3. Put your CA certificates and delegated signer files where the config points.
|
|
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 OCSP_REDIS_URL \
|
|
gitea.woggioni.net/woggioni/kca-ocsp:latest
|
|
```
|
|
|
|
It runs `FROM scratch` and contains only a statically linked musl executable
|
|
and a CA bundle for TLS connections. 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: `redis.cache_ttl_ms` (default 1000 ms).
|
|
- Concurrent requests for the same CA share one refresh. A Redis Lua script
|
|
performs `GET` and `PTTL` atomically, keeping the snapshot and its expiry
|
|
consistent.
|
|
- A missing or expired key produces `unknown`, as does a serial absent from an
|
|
existing snapshot. A key that exists without a TTL is rejected as a data
|
|
error. Redis connection and command failures produce `tryLater` when no
|
|
unexpired cached snapshot remains.
|
|
- If a refresh fails, a cached snapshot is served only until its Redis expiry.
|
|
A missing key clears cached status so it cannot be revived by a later Redis
|
|
failure.
|
|
- KCA republishes unchanged snapshots every
|
|
`KCA_OCSP_HEARTBEAT_INTERVAL_SECONDS` (default 30 seconds) and sets each
|
|
key's expiry using `KCA_OCSP_REDIS_EXPIRY_SECONDS` (default 60 seconds).
|
|
Keep the heartbeat comfortably below the key expiry.
|
|
- OCSP `thisUpdate` and `producedAt` use the response time. `nextUpdate` is
|
|
capped at the snapshot's Redis expiry when a snapshot is present.
|
|
- With multiple CAs, the cache TTL is per CA. If you need a global
|
|
one-request-per-second limit across all CAs, that requires a shared rate
|
|
limiter.
|