🚧 Add CA and server cert management
This commit is contained in:
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user