Files

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:
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:

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:

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.