#!/usr/bin/env python3 """Diagnostic script: verify SSL cert installation and encryption key health. Usage: docker cp scripts/diag_container.py superset-tools-backend-1:/tmp/ docker compose exec backend python3 /tmp/diag_container.py [--target https://lite.ai.rusal.com] """ import hashlib import os import ssl import subprocess import sys import traceback from pathlib import Path # ── Console helpers ────────────────────────────────────────────────── GREEN = "\033[32m" RED = "\033[31m" YELLOW = "\033[33m" BOLD = "\033[1m" RESET = "\033[0m" def ok(msg: str) -> None: print(f" {GREEN}✅{RESET} {msg}") def fail(msg: str) -> None: print(f" {RED}❌{RESET} {msg}") def warn(msg: str) -> None: print(f" {YELLOW}⚠️ {RESET} {msg}") def section(title: str) -> None: print(f"\n{BOLD}{'=' * 60}{RESET}") print(f"{BOLD} {title}{RESET}") print(f"{BOLD}{'=' * 60}{RESET}") # ── Section 1: Environment ─────────────────────────────────────────── def check_env() -> None: section("1. Environment variables") for var in ("ENCRYPTION_KEY", "AUTH_SECRET_KEY", "CERTS_PATH"): val = os.getenv(var, "") if not val: warn(f"{var} is NOT set") elif var in ("ENCRYPTION_KEY", "AUTH_SECRET_KEY"): fp = hashlib.sha256(val.encode()).hexdigest()[:8] ok(f"{var} is set (sha256:{fp})") else: ok(f"{var} = {val}") fp = hashlib.sha256(os.getenv("ENCRYPTION_KEY", "").encode()).hexdigest()[:8] print(f"\n Current key fingerprint: {BOLD}sha256:{fp}{RESET}") # ── Section 2: CERTS_PATH inventory ────────────────────────────────── def check_certs_inventory() -> None: section("2. CERTS_PATH inventory (/opt/certs)") from src.core.ssl import cert_dir_inventory inv = cert_dir_inventory("/opt/certs") if not inv["trust_candidates"] and not inv["server_files"]: warn("CERTS_PATH is empty or not mounted — no corporate CA certs found") print( " Put .crt/.pem/.cer/.der CA files in ./certs/ on the host and restart containers." ) return for name in inv["trust_candidates"]: ok(f"Trust candidate: {Path(name).name}") for name in inv["server_files"]: ok(f"Server file (skipped as CA): {Path(name).name}") for name in inv["invalid"]: warn(f"Unrecognized file (skipped): {Path(name).name}") # ── Section 3: System CA store ────────────────────────────────────── def check_system_certs() -> None: section("3. System CA store (/etc/ssl/certs/)") bundle = Path("/etc/ssl/certs/ca-certificates.crt") if not bundle.exists(): fail("ca-certificates.crt NOT found — cert installation failed fatally") return cert_count = subprocess.run( ["grep", "-c", "BEGIN CERTIFICATE", str(bundle)], capture_output=True, text=True, ).stdout.strip() ok(f"ca-certificates.crt exists, {cert_count} certificates in bundle") custom_pems = list(Path("/usr/local/share/ca-certificates/custom").rglob("*.crt")) if custom_pems: names = [p.name for p in custom_pems] ok(f"Installed CA certs: {', '.join(names)}") else: warn("No custom .crt files in /usr/local/share/ca-certificates/custom/") certs_dir = Path("/etc/ssl/certs") hash_links = [p for p in certs_dir.iterdir() if p.is_symlink()] custom_links = [l for l in hash_links if "custom" in str(Path(os.readlink(str(l))))] if custom_links: ok(f"Hash symlinks for custom certs: {len(custom_links)}") else: warn("No hash symlinks for custom certs — capath may not find them") # ── Section 4: OpenSSL connectivity ────────────────────────────────── def check_openssl(target: str) -> None: section(f"4. OpenSSL connectivity ({target})") capath = "/etc/ssl/certs/" host, port = _parse_target(target) r = subprocess.run( [ "openssl", "s_client", "-connect", f"{host}:{port}", "-servername", host, "-CApath", capath, ], input="", capture_output=True, text=True, timeout=15, ) if "Verify return code: 0" in (r.stderr + r.stdout): ok(f"capath: verify OK — cert chain trusted") else: fail( f"capath: verify FAILED.\n" f" The CA certificate for {host} is NOT in /etc/ssl/certs/.\n" f" Put the issuing/root CA .crt into CERTS_PATH (./certs)\n" f" and restart the container." ) def _parse_target(target: str) -> tuple[str, int]: if "://" in target: target = target.split("://")[1] if ":" in target: host, port = target.rsplit(":", 1) return host, int(port) return target, 443 # ── Section 5: Python SSL context ──────────────────────────────────── def check_python_ssl(target: str) -> None: section(f"5. Python SSL context ({target})") from src.core.ssl import system_ssl_context ctx = system_ssl_context() host, port = _parse_target(target) try: with ctx.wrap_socket(__import__("socket").socket(), server_hostname=host) as s: s.settimeout(10) s.connect((host, port)) cert = s.getpeercert() cn = dict(x[0] for x in cert.get("subject", [])) ok(f"SSLContext(capath): connected OK — CN={cn.get('commonName', '?')}") except Exception as e: fail(f"SSLContext(capath): FAILED — {e}") # ── Section 6: httpx connectivity ──────────────────────────────────── def check_httpx(target: str) -> None: section(f"6. httpx connectivity ({target})") try: import httpx from src.core.ssl import httpx_verify ctx = httpx_verify() url = f"https://{target}" if not target.startswith("http") else target r = httpx.get(url, verify=ctx, timeout=10, follow_redirects=True) ok(f"httpx(capath): HTTP {r.status_code}") except ImportError: warn("httpx not installed — skipping") except Exception as e: fail(f"httpx(capath): FAILED — {e}") # ── Section 7: Encryption health ───────────────────────────────────── def check_encryption_health() -> None: section("7. Encryption key health (internal)") sys.path.insert(0, "/app/backend") try: from src.core.encryption import get_encryption_manager, is_fernet_token from src.core.database import SessionLocal from src.models.llm import LLMProvider mgr = get_encryption_manager() db = SessionLocal() try: providers = db.query(LLMProvider).all() ok(f"LLM providers found: {len(providers)}") for p in providers: if not p.api_key: warn(f" {p.name}: api_key is EMPTY") continue if not is_fernet_token(p.api_key): warn(f" {p.name}: api_key is NOT a fernet token") continue try: mgr.decrypt(p.api_key) ok(f" {p.name}: api_key decrypts OK (healthy)") except Exception: fail( f" {p.name}: api_key decrypt FAILED — " f"encrypted with different ENCRYPTION_KEY" ) finally: db.close() except ImportError as e: warn(f"Cannot import backend modules: {e}") except Exception as e: fail(f"Encryption health check failed: {e}") # ── Main ───────────────────────────────────────────────────────────── def main() -> None: target = "lite.ai.rusal.com:443" args = sys.argv[1:] i = 0 while i < len(args): if args[i] == "--target" and i + 1 < len(args): target = args[i + 1] i += 2 elif args[i].startswith("--"): print(f"Unknown flag: {args[i]}") print(f"Usage: {sys.argv[0]} [--target host:port]") sys.exit(1) else: i += 1 print(f"\n{BOLD}superset-tools container diagnostic{RESET}") print(f"Target: {target}\n") check_env() check_certs_inventory() check_system_certs() check_openssl(target) check_python_ssl(target) check_httpx(target) check_encryption_health() section("Summary") print( "\nIf cert verification failures:\n" " → Put the issuing/root CA for target into CERTS_PATH (./certs)\n" " → Restart affected containers\n" " → Rerun this diagnostic\n" ) print( "\nIf ENCRYPTION_KEY health failures:\n" " → Stored secrets encrypted with old key\n" " → Open Settings → System → 'Encryption Key Recovery'\n" " → Re-enter LLM API keys and DB passwords\n" ) if __name__ == "__main__": try: main() except Exception: print(f"\n{RED}Script crashed:{RESET}") traceback.print_exc() sys.exit(1)