89 lines
3.0 KiB
Python
89 lines
3.0 KiB
Python
"""Utilities for SSL certificate hot-reloading."""
|
|
|
|
import asyncio
|
|
import logging
|
|
import ssl
|
|
from typing import Optional
|
|
|
|
from watchfiles import awatch
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class SSLCertRefresher:
|
|
"""Monitors SSL certificate files and reloads them when changed.
|
|
|
|
Uses ``watchfiles.awatch()`` for efficient inotify/kqueue-based
|
|
file monitoring. On change the referenced :class:`ssl.SSLContext`
|
|
is updated in-place so that new TLS connections automatically pick
|
|
up the fresh certificates.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
ssl_context: ssl.SSLContext,
|
|
key_path: str,
|
|
cert_path: str,
|
|
ca_path: Optional[str] = None,
|
|
) -> None:
|
|
self._ssl_context = ssl_context
|
|
self._key_path = key_path
|
|
self._cert_path = cert_path
|
|
self._ca_path = ca_path
|
|
self._tasks: list[asyncio.Task] = []
|
|
|
|
loop = asyncio.get_running_loop()
|
|
self._tasks.append(
|
|
loop.create_task(self._watch_cert_key(), name="ssl-cert-key-watcher")
|
|
)
|
|
if self._ca_path:
|
|
self._tasks.append(
|
|
loop.create_task(self._watch_ca(), name="ssl-ca-watcher")
|
|
)
|
|
|
|
async def _watch_cert_key(self) -> None:
|
|
"""Watch cert and key files and reload on change."""
|
|
try:
|
|
async for _changes in awatch(self._cert_path, self._key_path):
|
|
logger.info(
|
|
"SSL cert/key file change detected, reloading: " "cert=%s key=%s",
|
|
self._cert_path,
|
|
self._key_path,
|
|
)
|
|
try:
|
|
self._ssl_context.load_cert_chain(self._cert_path, self._key_path)
|
|
logger.info("SSL cert/key reloaded successfully.")
|
|
except Exception:
|
|
logger.exception(
|
|
"Failed to reload SSL cert/key — continuing with "
|
|
"previous certificates."
|
|
)
|
|
except asyncio.CancelledError:
|
|
return
|
|
|
|
async def _watch_ca(self) -> None:
|
|
"""Watch CA file and reload on change."""
|
|
assert self._ca_path is not None
|
|
try:
|
|
async for _changes in awatch(self._ca_path):
|
|
logger.info(
|
|
"SSL CA file change detected, reloading: ca=%s",
|
|
self._ca_path,
|
|
)
|
|
try:
|
|
self._ssl_context.load_verify_locations(self._ca_path)
|
|
logger.info("SSL CA certificates reloaded successfully.")
|
|
except Exception:
|
|
logger.exception(
|
|
"Failed to reload SSL CA certificates — continuing "
|
|
"with previous CA bundle."
|
|
)
|
|
except asyncio.CancelledError:
|
|
return
|
|
|
|
def stop(self) -> None:
|
|
"""Cancel all watching tasks."""
|
|
for task in self._tasks:
|
|
task.cancel()
|
|
self._tasks.clear()
|