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.
This commit is contained in:
@@ -191,6 +191,7 @@ class Settings:
|
||||
ocsp_s3_endpoint: Optional[str]
|
||||
ocsp_s3_force_path_style: bool
|
||||
ocsp_publish_interval_seconds: int
|
||||
ocsp_heartbeat_interval_seconds: int
|
||||
|
||||
@staticmethod
|
||||
def from_env() -> "Settings":
|
||||
@@ -245,6 +246,9 @@ class Settings:
|
||||
ocsp_publish_interval_seconds=max(
|
||||
1, int(_env("KCA_OCSP_PUBLISH_INTERVAL_SECONDS", "1"))
|
||||
),
|
||||
ocsp_heartbeat_interval_seconds=max(
|
||||
1, int(_env("KCA_OCSP_HEARTBEAT_INTERVAL_SECONDS", "30"))
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -10,11 +10,12 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any, Awaitable, Callable, Dict, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from kaya.core import KayaApp, KayaMixin
|
||||
from tortoise.expressions import Q
|
||||
|
||||
from .config import Settings, settings
|
||||
from .models import Certificate, CertificateRevocation, OcspSnapshot, Vault
|
||||
@@ -147,14 +148,16 @@ async def mark_all_cas_dirty(vault: Vault) -> None:
|
||||
|
||||
|
||||
class OcspPublisherMixin(KayaMixin):
|
||||
"""Background publisher for dirty OCSP snapshots."""
|
||||
"""Background publisher for dirty and due OCSP snapshots."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
interval_seconds: int,
|
||||
ensure_context: Callable[[], Awaitable[None]],
|
||||
heartbeat_interval_seconds: int = 30,
|
||||
) -> None:
|
||||
self._interval = max(1, interval_seconds)
|
||||
self._heartbeat_interval = max(1, heartbeat_interval_seconds)
|
||||
self._ensure_context = ensure_context
|
||||
self._task: Optional[asyncio.Task] = None
|
||||
self._bootstrapped = False
|
||||
@@ -212,7 +215,14 @@ class OcspPublisherMixin(KayaMixin):
|
||||
# The publisher runs outside an HTTP request task, so it must bind
|
||||
# Tortoise's per-loop context before querying models.
|
||||
await self._ensure_context()
|
||||
rows = await OcspSnapshot.filter(dirty=True).limit(25)
|
||||
heartbeat_before = datetime.now(timezone.utc) - timedelta(
|
||||
seconds=self._heartbeat_interval
|
||||
)
|
||||
rows = await OcspSnapshot.filter(
|
||||
Q(dirty=True)
|
||||
| Q(published_at__isnull=True)
|
||||
| Q(published_at__lte=heartbeat_before)
|
||||
).limit(25)
|
||||
for row in rows:
|
||||
ca = await Certificate.get_or_none(id=row.ca_id)
|
||||
if ca is None:
|
||||
@@ -226,7 +236,7 @@ class OcspPublisherMixin(KayaMixin):
|
||||
published_at = datetime.now(timezone.utc)
|
||||
cleared = await OcspSnapshot.filter(
|
||||
ca_id=row.ca_id,
|
||||
dirty=True,
|
||||
dirty=row.dirty,
|
||||
updated_at=row.updated_at,
|
||||
).update(
|
||||
dirty=False,
|
||||
@@ -257,6 +267,7 @@ def publisher_mixin_from_settings(
|
||||
return OcspPublisherMixin(
|
||||
settings_obj.ocsp_publish_interval_seconds,
|
||||
ensure_context,
|
||||
settings_obj.ocsp_heartbeat_interval_seconds,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user