Publish OCSP snapshots through Redis
This commit is contained in:
+40
-52
@@ -1,58 +1,47 @@
|
||||
# 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.
|
||||
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**, no Redis access, and no KCA master key. It
|
||||
only needs:
|
||||
It has **no KCA database access** and no KCA master key. It only needs:
|
||||
|
||||
- `s3:GetObject` on the configured bucket/prefix;
|
||||
- read access to OCSP keys in the same Redis instance and logical database KCA
|
||||
uses for sessions;
|
||||
- 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. KCA needs access to both
|
||||
`ocsp:*` and `kaya:session:*`.
|
||||
|
||||
## Snapshot layout
|
||||
|
||||
KCA publishes objects as:
|
||||
KCA publishes keys as:
|
||||
|
||||
```
|
||||
<prefix>/<KCA CA UUID>.json
|
||||
<prefix>:<KCA CA UUID>
|
||||
```
|
||||
|
||||
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.
|
||||
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 objects when `KCA_OCSP_S3_BUCKET` is configured. The
|
||||
KCA publishes these keys when `REDIS_URL` is configured. The
|
||||
`kca-ocsp-status/v2` JSON contains all certificates issued by that CA, both
|
||||
valid and revoked, without a generation timestamp. Freshness is carried by the
|
||||
S3 `Expires` header instead. 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.
|
||||
|
||||
## Upgrading from timestamped snapshots
|
||||
|
||||
Version 2 snapshots remove `generated_at` and require an S3 `Expires` header.
|
||||
Upgrade the publisher and responder together: old objects have no `Expires` and
|
||||
are rejected, while old responders cannot parse the version 2 JSON. Until the
|
||||
new publisher writes a snapshot, the new responder returns `tryLater`.
|
||||
Redis key TTL. The responder never infers status from a partial revoked-only
|
||||
list.
|
||||
|
||||
## 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.
|
||||
2. Set `redis.url` or `OCSP_REDIS_URL` to the same Redis URL used by KCA.
|
||||
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:
|
||||
@@ -70,12 +59,12 @@ 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 \
|
||||
-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 outbound S3 HTTPS. There is no shell or package manager in
|
||||
and a CA bundle for TLS connections. There is no shell or package manager in
|
||||
the image.
|
||||
|
||||
## Verify with OpenSSL
|
||||
@@ -104,24 +93,23 @@ build a trust store containing the CA or validate with your normal PKI tooling.
|
||||
|
||||
## Cache and availability
|
||||
|
||||
- Per-CA cache TTL: `s3.cache_ttl_ms` (default 1000 ms).
|
||||
- Concurrent requests for the same CA share one refresh. Each refresh performs
|
||||
`HeadObject` to obtain `Expires` and the ETag, then `GetObject` with
|
||||
`If-Match` for that ETag. This supports RustFS, which does not return the
|
||||
stored `Expires` value from its local `GetObject` path, and prevents mixing
|
||||
metadata and bodies from different object versions. Concurrent overwrites
|
||||
cause a bounded retry of the complete HEAD/GET sequence.
|
||||
- `Expires` is the hard snapshot expiry. Missing, malformed and expired values
|
||||
are rejected; if no unexpired cached snapshot remains, OCSP returns
|
||||
`tryLater`. The header controls responder freshness only—S3 does not delete
|
||||
the object at that time.
|
||||
- If a refresh fails, a cached snapshot is served only until its own `Expires`
|
||||
timestamp, regardless of how recently it was fetched.
|
||||
- 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 `Expires`
|
||||
using `KCA_OCSP_OBJECT_EXPIRY_SECONDS` (default 60 seconds). Keep the
|
||||
heartbeat comfortably below the object expiry.
|
||||
`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 hard expiry.
|
||||
- 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.
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user