🚧 Add CA and server cert management

This commit is contained in:
Jonas Bengtsson
2026-09-19 13:35:49 +02:00
parent 45de68f0b7
commit 111d6cc390
20 changed files with 1317 additions and 34 deletions
+49 -1
View File
@@ -8,6 +8,7 @@ an immutable revision (spec section 26).
from __future__ import annotations
import os
import uuid
from datetime import UTC, datetime
from typing import Annotated
@@ -24,6 +25,7 @@ from app.db import get_db
from app.deps import CurrentUser, require_roles
from app.models import (
BrokerSettings,
Certificate,
ConfigRevision,
ListenerConfig,
ManagementRole,
@@ -207,6 +209,51 @@ def upsert_setting(
# --- Generation, validation, deployment ----------------------------------------
# The sidecar and broker share the mosquitto_certs volume; the backend
# publishes the DECRYPTED material there because Mosquitto needs
# plaintext files (uid 1883, mode 600). The DB keeps the encrypted master.
CERTS_DIR = os.environ.get("MOSQUITTO_CERTS_DIR", "/mosquitto/certs")
def _publish_certificates(db: Session) -> None:
"""Write cert/key/CA files for every TLS listener's bound certificate.
Must run BEFORE the config referencing them is validated/activated,
otherwise `mosquitto --test-config` fails on missing files.
"""
listeners = db.scalars(
select(ListenerConfig).where(ListenerConfig.tls_enabled.is_(True))
).all()
seen: set[uuid.UUID] = set()
for listener in listeners:
if listener.cert_id is None or listener.cert_id in seen:
continue
seen.add(listener.cert_id)
cert = db.get(Certificate, listener.cert_id)
if cert is None or cert.private_key_pem is None:
continue # nothing to publish (warned about in configgen)
os.makedirs(CERTS_DIR, exist_ok=True)
cert_path = os.path.join(CERTS_DIR, f"{cert.id}.crt")
with open(cert_path, "w", encoding="utf-8") as f:
f.write(cert.certificate_pem)
os.chmod(cert_path, 0o644)
# Private key: decrypted for the broker (DB keeps the AES-GCM copy)
from app.crypto import decrypt_secret
key_path = os.path.join(CERTS_DIR, f"{cert.id}.key")
with open(key_path, "w", encoding="utf-8") as f:
f.write(decrypt_secret(cert.private_key_pem))
os.chmod(key_path, 0o600)
# Issuing CA certificate (for cafile)
if cert.parent_id is not None:
ca = db.get(Certificate, cert.parent_id)
if ca is not None:
ca_path = os.path.join(CERTS_DIR, f"{ca.id}.crt")
with open(ca_path, "w", encoding="utf-8") as f:
f.write(ca.certificate_pem)
os.chmod(ca_path, 0o644)
@router.get("/config/preview", response_model=ConfigPreviewOut)
def preview_config(
db: Annotated[Session, Depends(get_db)], _user: AnyReader
@@ -235,7 +282,8 @@ def deploy_config(
db: Annotated[Session, Depends(get_db)],
user: BrokerAdmin,
):
"""Generate -> validate -> activate + reload -> record revision."""
"""Generate -> publish certs -> validate -> activate + reload -> revision."""
_publish_certificates(db)
result = generate_config(db)
response = _call_control("/deploy", json_body={"content": result.config_text})
if response.get("status") != "deployed":