Files
ss-tools/backend/src/core/ssl.py
busya 1e61f098bd fix(docker): add missing certs.sh COPY to backend/all-in-one Dockerfile
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
2026-07-07 00:58:41 +03:00

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