Allow dedicated Redis for OCSP publication

This commit is contained in:
2026-10-31 20:26:37 +00:00
parent ef64c4a532
commit a101182fdd
10 changed files with 93 additions and 33 deletions
@@ -167,6 +167,7 @@ class Settings:
app_host: str
app_port: int
redis_url: Optional[str]
ocsp_redis_url: Optional[str]
static_dir: str
logging_config: Optional[str]
#: AES-256-GCM master key protecting private keys at rest.
@@ -195,6 +196,11 @@ class Settings:
ocsp_heartbeat_interval_seconds: int
ocsp_redis_expiry_seconds: int
@property
def ocsp_effective_redis_url(self) -> Optional[str]:
"""Dedicated OCSP Redis when configured, otherwise the session Redis."""
return self.ocsp_redis_url or self.redis_url
@staticmethod
def from_env() -> "Settings":
raw_master = os.environ.get("KCA_MASTER_KEY")
@@ -226,6 +232,7 @@ class Settings:
app_host=_env("APP_HOST", "0.0.0.0"),
app_port=int(_env("APP_PORT", "8080")),
redis_url=os.environ.get("REDIS_URL") or None,
ocsp_redis_url=os.environ.get("KCA_OCSP_REDIS_URL") or None,
static_dir=_env("STATIC_DIR", "web/dist"),
logging_config=os.environ.get("LOGGING_CONFIG") or None,
master_key=master_key,
+13 -11
View File
@@ -1,9 +1,8 @@
"""Per-CA OCSP status snapshots and Redis publication.
KCA is the source of truth for certificate status. This module builds a
compact CBOR document per CA and stores it in the same Redis instance used for
sessions, so the standalone OCSP responder never needs database access or a CA
private key.
compact CBOR document per CA and stores it in the selected Redis instance, so
the standalone OCSP responder never needs database access or a CA private key.
"""
from __future__ import annotations
@@ -97,9 +96,9 @@ async def publish_snapshot(
owned_client = None
if client is None:
if not settings.redis_url:
if not settings.ocsp_effective_redis_url:
raise RuntimeError("REDIS_URL is not configured")
client = Redis.from_url(settings.redis_url)
client = Redis.from_url(settings.ocsp_effective_redis_url)
owned_client = client
try:
await client.set(key, body, ex=settings.ocsp_redis_expiry_seconds)
@@ -112,7 +111,7 @@ async def publish_snapshot(
async def mark_ocsp_dirty(ca_id: UUID) -> None:
"""Mark one issuing CA's status snapshot as needing a republish."""
if not settings.redis_url:
if not settings.ocsp_effective_redis_url:
return
row = await OcspSnapshot.get_or_none(ca_id=ca_id)
if row is None:
@@ -126,7 +125,7 @@ async def mark_ocsp_dirty(ca_id: UUID) -> None:
async def mark_all_cas_dirty(vault: Vault) -> None:
if not settings.redis_url:
if not settings.ocsp_effective_redis_url:
return
for ca in await Certificate.filter(vault_id=vault.id, is_ca=True):
await mark_ocsp_dirty(ca.id)
@@ -155,11 +154,14 @@ class OcspPublisherMixin(KayaMixin):
return None
def setup(self, loop: asyncio.AbstractEventLoop) -> None:
if not settings.redis_url:
log.info("OCSP Redis publication disabled (REDIS_URL unset)")
if not settings.ocsp_effective_redis_url:
log.info(
"OCSP Redis publication disabled "
"(REDIS_URL and KCA_OCSP_REDIS_URL unset)"
)
return
if self._redis_client is None:
self._redis_client = Redis.from_url(settings.redis_url)
self._redis_client = Redis.from_url(settings.ocsp_effective_redis_url)
self._owns_redis_client = True
self._task = loop.create_task(self._poll())
log.info(
@@ -249,7 +251,7 @@ def publisher_mixin_from_settings(
settings_obj: Settings,
ensure_context: Callable[[], Awaitable[None]],
) -> Optional[OcspPublisherMixin]:
if not settings_obj.redis_url:
if not settings_obj.ocsp_effective_redis_url:
return None
return OcspPublisherMixin(
settings_obj.ocsp_publish_interval_seconds,
@@ -155,9 +155,9 @@ def register(app: KayaApp, kca: "Kca") -> None:
row = await OcspSnapshot.get_or_none(ca_id=cert.id)
snapshot = await snapshot_for_ca(cert)
await send_json(ctx, 200, {
"enabled": bool(settings.redis_url),
"enabled": bool(settings.ocsp_effective_redis_url),
"prefix": settings.ocsp_redis_prefix,
"dirty": row.dirty if row else bool(settings.redis_url),
"dirty": row.dirty if row else bool(settings.ocsp_effective_redis_url),
"published_at": row.published_at.isoformat() if row and row.published_at else None,
"last_error": row.last_error if row else None,
"snapshot": snapshot,