Backend entrypoint sources certs.sh for CA certificate installation,
but the file was never copied into the Docker image — causing
container crash loop on startup (regression in 8fd23f7e).
Changes:
- docker/backend.Dockerfile, docker/all-in-one.Dockerfile: COPY certs.sh
- docker/backend.entrypoint.sh: remove dead install_llm_ca_certs,
install_certificates (replaced by docker/certs.sh); update @INVARIANT
- backend/src/core/ssl.py: fix describe_context() to report actual
system cert count (SSLContext.capath attr does not exist in Python 3)
- __tests__: rewrite stale tests asserting LLM_SSL_VERIFY=false →
verify=False; behaviour is permanently removed — always CERT_REQUIRED
- backend/tests/integration/test_backend_container.py [new]: 5 tests
verifying certs.sh presence, sourceability, and full-stack smoke
(testcontainers PG → entrypoint → migrations → health)
- conftest.py: restore health-wait loop and fixtures in superset_container
- ADR-0009, README.md, scripts, .env: align docs with centralized SSL
104 lines
4.1 KiB
Python
104 lines
4.1 KiB
Python
# #region CoreSslTrust [C:4] [TYPE Module] [SEMANTICS ssl,tls,trust,certs,security]
|
|
# @BRIEF Centralized SSL/TLS trust context for all Python HTTP clients.
|
|
# @LAYER Infrastructure
|
|
# @INVARIANT Never returns verify=False from environment configuration.
|
|
# The application does not disable TLS verification via env var.
|
|
# All trust anchors come from the system CA store populated
|
|
# at container startup via CERTS_PATH (/opt/certs) mount.
|
|
# @RATIONALE Previously LLM_SSL_VERIFY, LLM_CA_CERT_URLS, and per-client
|
|
# _get_verify / _get_ssl_verify functions were scattered across
|
|
# llm_analysis, translate, and agent code. This centralizes
|
|
# them into one contract: system CA store is the single source
|
|
# of trust, populated once per container at startup.
|
|
# @REJECTED Per-client SSL verify toggle was rejected — caused operators
|
|
# to manage LLM TLS separately from Superset/Git/backend TLS,
|
|
# and allowed LLM_SSL_VERIFY=false to become a permanent
|
|
# workaround instead of fixing cert installation.
|
|
|
|
import os
|
|
import ssl
|
|
from pathlib import Path
|
|
|
|
DEFAULT_CAPATH = "/etc/ssl/certs"
|
|
|
|
|
|
# ── Public API ────────────────────────────────────────────────────────
|
|
|
|
|
|
def system_ssl_context() -> ssl.SSLContext:
|
|
"""Return an SSLContext that trusts the system CA store.
|
|
|
|
Prefers capath over cafile because OpenSSL 3.x ignores intermediate
|
|
CA certificates in flat bundle files (ADR-0009 Finding 7).
|
|
Falls back to default context if capath directory is not available
|
|
(rare edge case, e.g. minimal containers without ca-certificates).
|
|
"""
|
|
if os.path.isdir(DEFAULT_CAPATH):
|
|
return ssl.create_default_context(capath=DEFAULT_CAPATH)
|
|
return ssl.create_default_context()
|
|
|
|
|
|
def httpx_verify() -> ssl.SSLContext:
|
|
"""Return SSL verification context for httpx clients.
|
|
|
|
Alias for system_ssl_context() to provide a clear API for the
|
|
main HTTP library used across the project (httpx).
|
|
"""
|
|
return system_ssl_context()
|
|
|
|
|
|
def describe_context(ctx: ssl.SSLContext | bool | None) -> str:
|
|
"""Return a diagnostic description of the SSL verification state.
|
|
|
|
Does NOT leak key material or secret values.
|
|
SSLContext in Python 3.13 does not expose capath/cafile as public
|
|
attributes — verify_load_locations info is internal to OpenSSL.
|
|
Instead, reports whether /etc/ssl/certs (DEFAULT_CAPATH) is available
|
|
and the cert count in the system bundle.
|
|
"""
|
|
if ctx is False:
|
|
return "DISABLED (verify=False — should not happen in production)"
|
|
if ctx is None:
|
|
return "DEFAULT (certifi)"
|
|
if isinstance(ctx, ssl.SSLContext):
|
|
bundle = Path(DEFAULT_CAPATH) / "ca-certificates.crt"
|
|
cert_count = 0
|
|
if bundle.exists():
|
|
cert_count = bundle.read_bytes().count(b"-----BEGIN CERTIFICATE-----")
|
|
mode = "CERT_REQUIRED" if ctx.verify_mode == ssl.CERT_REQUIRED else str(ctx.verify_mode)
|
|
return f"SSLContext(verify_mode={mode}, system_certs={cert_count})"
|
|
return f"unknown({type(ctx).__name__})"
|
|
|
|
|
|
def cert_dir_inventory(certs_path: str = "/opt/certs") -> dict:
|
|
"""Inventory /opt/certs for operator diagnostics.
|
|
|
|
Returns counts of recognized trust candidates, server/private files,
|
|
and unrecognized files.
|
|
"""
|
|
result: dict[str, list[str]] = {
|
|
"trust_candidates": [],
|
|
"server_files": [],
|
|
"invalid": [],
|
|
}
|
|
base = Path(certs_path)
|
|
if not base.is_dir():
|
|
return result
|
|
|
|
for f in sorted(base.iterdir()):
|
|
if not f.is_file():
|
|
continue
|
|
name = f.name.lower()
|
|
if name in ("server.crt", "server.key", "server.pem") or any(name.endswith(ext) for ext in (".key", ".p12", ".pfx")):
|
|
result["server_files"].append(str(f))
|
|
continue
|
|
if any(name.endswith(ext) for ext in (".crt", ".pem", ".cer", ".der")):
|
|
result["trust_candidates"].append(str(f))
|
|
else:
|
|
result["invalid"].append(str(f))
|
|
|
|
return result
|
|
|
|
|
|
# #endregion CoreSslTrust
|