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
This commit is contained in:
@@ -34,10 +34,15 @@ results: dict = {}
|
||||
|
||||
# ── Helpers ──
|
||||
|
||||
def _run(cmd: list[str], timeout: int = 15, input_data: str = "") -> tuple[int, str, str]:
|
||||
|
||||
def _run(
|
||||
cmd: list[str], timeout: int = 15, input_data: str = ""
|
||||
) -> tuple[int, str, str]:
|
||||
"""Run subprocess with optional stdin."""
|
||||
try:
|
||||
r = subprocess.run(cmd, input=input_data, capture_output=True, text=True, timeout=timeout)
|
||||
r = subprocess.run(
|
||||
cmd, input=input_data, capture_output=True, text=True, timeout=timeout
|
||||
)
|
||||
return r.returncode, r.stdout, r.stderr
|
||||
except FileNotFoundError:
|
||||
return -1, "", "command not found"
|
||||
@@ -55,10 +60,18 @@ def _count_certs(path: str) -> int:
|
||||
|
||||
# ── Openssl tests ──
|
||||
|
||||
|
||||
def test_openssl(label: str, extra_args: list[str]) -> None:
|
||||
"""Test openssl s_client with empty stdin to prevent hang."""
|
||||
cmd = ["openssl", "s_client", "-connect", f"{host}:443",
|
||||
"-servername", host, "-verify_return_error"] + extra_args
|
||||
cmd = [
|
||||
"openssl",
|
||||
"s_client",
|
||||
"-connect",
|
||||
f"{host}:443",
|
||||
"-servername",
|
||||
host,
|
||||
"-verify_return_error",
|
||||
] + extra_args
|
||||
rc, out, err = _run(cmd, timeout=20, input_data="")
|
||||
for line in out.split("\n"):
|
||||
if "Verify return code" in line:
|
||||
@@ -72,9 +85,11 @@ def test_openssl(label: str, extra_args: list[str]) -> None:
|
||||
|
||||
# ── Python library tests ──
|
||||
|
||||
|
||||
def test_httpx(key: str, verify) -> None:
|
||||
try:
|
||||
import httpx
|
||||
|
||||
r = httpx.get(TARGET_URL, verify=verify, timeout=10)
|
||||
results[key] = "OK"
|
||||
print(f" {key}: ✅ status={r.status_code}")
|
||||
@@ -86,6 +101,7 @@ def test_httpx(key: str, verify) -> None:
|
||||
def test_requests(key: str, verify) -> None:
|
||||
try:
|
||||
import requests as req
|
||||
|
||||
r = req.get(TARGET_URL, verify=verify, timeout=10)
|
||||
results[key] = "OK"
|
||||
print(f" {key}: ✅ status={r.status_code}")
|
||||
@@ -100,11 +116,12 @@ def test_requests(key: str, verify) -> None:
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--target", default=os.environ.get("TARGET_URL", "https://lite.ai.rusal.com"))
|
||||
parser.add_argument(
|
||||
"--target", default=os.environ.get("TARGET_URL", "https://lite.ai.rusal.com")
|
||||
)
|
||||
args = parser.parse_args()
|
||||
TARGET_URL = args.target
|
||||
host = TARGET_URL.replace("https://", "").split("/")[0]
|
||||
LLM_SSL_VERIFY = os.environ.get("LLM_SSL_VERIFY", "true")
|
||||
|
||||
# ── 1. System ──
|
||||
print("\n=== 1. System ===")
|
||||
@@ -117,7 +134,9 @@ if __name__ == "__main__":
|
||||
v = getattr(mod, "__version__", "installed")
|
||||
p = getattr(mod, "__file__", "")
|
||||
if lib == "certifi":
|
||||
print(f" {lib}={v} path={mod.where()} certs={_count_certs(mod.where())}")
|
||||
print(
|
||||
f" {lib}={v} path={mod.where()} certs={_count_certs(mod.where())}"
|
||||
)
|
||||
else:
|
||||
print(f" {lib}={v}")
|
||||
except ImportError:
|
||||
@@ -136,11 +155,24 @@ if __name__ == "__main__":
|
||||
continue
|
||||
for f in sorted(d.iterdir()):
|
||||
if f.suffix in (".pem", ".crt") and f.is_file():
|
||||
rc, out, _ = _run(["openssl", "x509", "-in", str(f), "-noout",
|
||||
"-subject", "-issuer"], timeout=3)
|
||||
rc, out, _ = _run(
|
||||
["openssl", "x509", "-in", str(f), "-noout", "-subject", "-issuer"],
|
||||
timeout=3,
|
||||
)
|
||||
if rc != 0:
|
||||
rc, out, _ = _run(["openssl", "x509", "-in", str(f), "-inform",
|
||||
"DER", "-noout", "-subject"], timeout=3)
|
||||
rc, out, _ = _run(
|
||||
[
|
||||
"openssl",
|
||||
"x509",
|
||||
"-in",
|
||||
str(f),
|
||||
"-inform",
|
||||
"DER",
|
||||
"-noout",
|
||||
"-subject",
|
||||
],
|
||||
timeout=3,
|
||||
)
|
||||
subj = out.replace("subject=", "").strip() if rc == 0 else "UNREADABLE"
|
||||
print(f" {f.parent.name}/{f.name} subj={subj[:60]}")
|
||||
|
||||
@@ -154,11 +186,21 @@ if __name__ == "__main__":
|
||||
print("\n=== 4. Python httpx (LLMClient) ===")
|
||||
try:
|
||||
import httpx
|
||||
|
||||
test_httpx("httpx_True", verify=True)
|
||||
try:
|
||||
import ssl
|
||||
ctx_cafile = ssl.create_default_context(cafile=str(CA_BUNDLE)) if CA_BUNDLE.exists() else ssl.create_default_context()
|
||||
ctx_capath = ssl.create_default_context(capath=str(CA_DIR)) if CA_DIR.is_dir() else ssl.create_default_context()
|
||||
|
||||
ctx_cafile = (
|
||||
ssl.create_default_context(cafile=str(CA_BUNDLE))
|
||||
if CA_BUNDLE.exists()
|
||||
else ssl.create_default_context()
|
||||
)
|
||||
ctx_capath = (
|
||||
ssl.create_default_context(capath=str(CA_DIR))
|
||||
if CA_DIR.is_dir()
|
||||
else ssl.create_default_context()
|
||||
)
|
||||
test_httpx("httpx_cafile", verify=ctx_cafile)
|
||||
test_httpx("httpx_capath", verify=ctx_capath)
|
||||
except Exception as e:
|
||||
@@ -171,6 +213,7 @@ if __name__ == "__main__":
|
||||
print("\n=== 5. Python requests (translate plugin) ===")
|
||||
try:
|
||||
import requests as req
|
||||
|
||||
test_requests("requests_True", verify=True)
|
||||
test_requests("requests_cafile", verify=str(CA_BUNDLE))
|
||||
test_requests("requests_capath", verify=str(CA_DIR))
|
||||
@@ -182,7 +225,9 @@ if __name__ == "__main__":
|
||||
print("\n=== 6. NSS Chromium ===")
|
||||
rc, out, err = _run(["certutil", "-L", "-d", NSS_DB], timeout=5)
|
||||
if rc == 0:
|
||||
lines = [l for l in out.split("\n") if l.strip() and "Certificate Nickname" not in l]
|
||||
lines = [
|
||||
l for l in out.split("\n") if l.strip() and "Certificate Nickname" not in l
|
||||
]
|
||||
print(f" DB={NSS_DB} certs={len(lines)}")
|
||||
for l in lines:
|
||||
if any(x in l.lower() for x in ("rusal", "llm-", "custom-")):
|
||||
@@ -190,17 +235,38 @@ if __name__ == "__main__":
|
||||
else:
|
||||
print(f" NSS: {err.strip() or 'unavailable'}")
|
||||
|
||||
# ── 7. LLM_SSL_VERIFY env ──
|
||||
print("\n=== 7. LLM_SSL_VERIFY ===")
|
||||
print(f" LLM_SSL_VERIFY={LLM_SSL_VERIFY} disabled={LLM_SSL_VERIFY.strip().lower() in ('false','0','no','off')}")
|
||||
# ── 7. Centralized SSL (core.ssl) ──
|
||||
print("\n=== 7. Centralized SSL (core.ssl) ===")
|
||||
try:
|
||||
from src.core.ssl import system_ssl_context, describe_context
|
||||
|
||||
ctx = system_ssl_context()
|
||||
print(f" {describe_context(ctx)}")
|
||||
print(
|
||||
" LLM_SSL_VERIFY and LLM_CA_CERT_URLS removed — centralized CERTS_PATH only."
|
||||
)
|
||||
except ImportError:
|
||||
print(" src.core.ssl: UNAVAILABLE (backend not installed)")
|
||||
|
||||
# ── 8. JSON Summary ──
|
||||
print("\n=== 8. JSON Summary ===")
|
||||
summary = {k: results[k] for k in [
|
||||
"openssl_default", "openssl_cafile", "openssl_capath",
|
||||
"httpx_True", "httpx_cafile", "httpx_capath", "httpx_False",
|
||||
"requests_True", "requests_cafile", "requests_capath", "requests_False",
|
||||
] if k in results}
|
||||
summary = {
|
||||
k: results[k]
|
||||
for k in [
|
||||
"openssl_default",
|
||||
"openssl_cafile",
|
||||
"openssl_capath",
|
||||
"httpx_True",
|
||||
"httpx_cafile",
|
||||
"httpx_capath",
|
||||
"httpx_False",
|
||||
"requests_True",
|
||||
"requests_cafile",
|
||||
"requests_capath",
|
||||
"requests_False",
|
||||
]
|
||||
if k in results
|
||||
}
|
||||
|
||||
# Diagnosis
|
||||
diag = []
|
||||
@@ -212,7 +278,9 @@ if __name__ == "__main__":
|
||||
v = str(summary.get(k, ""))
|
||||
diag.append(f"{k}={'OK' if v.startswith('OK') else 'FAIL'}")
|
||||
|
||||
if str(results.get("httpx_capath", "")).startswith("OK") and not str(results.get("httpx_cafile", "")).startswith("OK"):
|
||||
if str(results.get("httpx_capath", "")).startswith("OK") and not str(
|
||||
results.get("httpx_cafile", "")
|
||||
).startswith("OK"):
|
||||
diag.append("VERDICT=capath_works_cafile_fails_use_capath")
|
||||
elif str(summary.get("httpx_False", "")).startswith("OK"):
|
||||
diag.append("VERDICT=all_ssl_fails_verify_disabled_works")
|
||||
|
||||
Reference in New Issue
Block a user