feat(server): add in-flight certificate validity watchdog and dynamic SSLContext reloading in Server.py
This commit is contained in:
+80
-4
@@ -322,6 +322,72 @@ def init_mtls_server_context(cert_dir: str = "certs") -> ssl.SSLContext:
|
|||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
def reload_mtls_context(ssl_ctx: ssl.SSLContext, cert_dir: str = "certs"):
|
||||||
|
"""
|
||||||
|
Dynamically reloads server certificate chain and Root CA in an active SSLContext.
|
||||||
|
Allows in-flight TLS certificate rotation without dropping the listening socket.
|
||||||
|
"""
|
||||||
|
ca_file = os.path.join(cert_dir, "ca.crt")
|
||||||
|
srv_cert = os.path.join(cert_dir, "server.crt")
|
||||||
|
srv_key = os.path.join(cert_dir, "server.key")
|
||||||
|
|
||||||
|
ssl_ctx.load_cert_chain(certfile=srv_cert, keyfile=srv_key)
|
||||||
|
ssl_ctx.load_verify_locations(cafile=ca_file)
|
||||||
|
|
||||||
|
|
||||||
|
def check_and_rotate_server_certs(
|
||||||
|
cert_dir: str = "certs",
|
||||||
|
hostnames: Optional[List[str]] = None,
|
||||||
|
threshold_days: int = 30
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Checks if Root CA or server TLS certificate are expiring within threshold_days.
|
||||||
|
If so, regenerates them, dynamically reloads the active SSLContext in-place,
|
||||||
|
and updates the server's in-memory CA reference so future enrollments use the new CA.
|
||||||
|
Returns True if renewed/reloaded, False otherwise.
|
||||||
|
"""
|
||||||
|
ca_renewed, srv_renewed = enrollment.check_and_renew_hub_pki(
|
||||||
|
cert_dir=cert_dir,
|
||||||
|
hostnames=hostnames,
|
||||||
|
threshold_days=threshold_days
|
||||||
|
)
|
||||||
|
if ca_renewed or srv_renewed:
|
||||||
|
print(f"[!] Server Hub PKI certificates renewed (CA renewed: {ca_renewed}, Server cert renewed: {srv_renewed}).")
|
||||||
|
ca_cert, ca_key, ca_pem, ca_key_pem = enrollment.generate_ca_if_needed(cert_dir=cert_dir, force_renew=False)
|
||||||
|
SERVER_STATE["ca_cert"] = ca_cert
|
||||||
|
SERVER_STATE["ca_key"] = ca_key
|
||||||
|
SERVER_STATE["ca_cert_pem"] = ca_pem
|
||||||
|
|
||||||
|
ssl_ctx = SERVER_STATE.get("ssl_ctx")
|
||||||
|
if ssl_ctx is not None:
|
||||||
|
try:
|
||||||
|
reload_mtls_context(ssl_ctx, cert_dir=cert_dir)
|
||||||
|
print("[+] In-flight mTLS SSLContext successfully reloaded with updated certificates.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[!] Failed to reload in-flight SSLContext: {e}")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def cert_validity_watchdog(interval_seconds: int = 43200, threshold_days: int = 30):
|
||||||
|
"""
|
||||||
|
Periodically checks the validity of Hub Root CA and Server TLS certificates (default every 12 hours).
|
||||||
|
Triggers in-flight renewal and dynamic context reloading if expiration is within threshold_days.
|
||||||
|
"""
|
||||||
|
config = SERVER_STATE.get("config", {})
|
||||||
|
cert_dir = config.get("cert_dir", "certs")
|
||||||
|
hostnames = [config.get("tcp_host", "0.0.0.0"), "127.0.0.1", "localhost"]
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
await asyncio.sleep(interval_seconds)
|
||||||
|
check_and_rotate_server_certs(cert_dir=cert_dir, hostnames=hostnames, threshold_days=threshold_days)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[!] Exception in cert_validity_watchdog: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def handle_socket_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
|
async def handle_socket_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
|
||||||
"""
|
"""
|
||||||
mTLS TCP socket handler.
|
mTLS TCP socket handler.
|
||||||
@@ -631,10 +697,20 @@ async def run_server():
|
|||||||
print(f"[*] Hermes Reporting API available at http://{hermes_host}:{hermes_port}/api/hermes/report")
|
print(f"[*] Hermes Reporting API available at http://{hermes_host}:{hermes_port}/api/hermes/report")
|
||||||
print(f"[*] Client Enrollment API available at http://{hermes_host}:{hermes_port}/api/client/enroll")
|
print(f"[*] Client Enrollment API available at http://{hermes_host}:{hermes_port}/api/client/enroll")
|
||||||
|
|
||||||
await asyncio.gather(
|
watchdog_task = asyncio.create_task(cert_validity_watchdog())
|
||||||
tcp_server.serve_forever(),
|
|
||||||
uv_server.serve()
|
try:
|
||||||
)
|
await asyncio.gather(
|
||||||
|
tcp_server.serve_forever(),
|
||||||
|
uv_server.serve(),
|
||||||
|
watchdog_task
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
watchdog_task.cancel()
|
||||||
|
try:
|
||||||
|
await watchdog_task
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
Reference in New Issue
Block a user