diff --git a/backend/main.py b/backend/main.py index 304ce42dd..557086682 100644 --- a/backend/main.py +++ b/backend/main.py @@ -41,6 +41,22 @@ logging.getLogger("httpx").setLevel(logging.WARNING) logging.getLogger("httpcore").setLevel(logging.WARNING) + +def _enable_system_trust_store() -> None: + try: + import truststore + + truststore.inject_into_ssl() + logging.getLogger("main").info("System trust store enabled") + except Exception: + logging.getLogger("main").warning( + "Failed to enable system trust store; falling back to default CA bundle", + exc_info=True, + ) + + +_enable_system_trust_store() + from app import api from app.component.environment import env from app.router import register_routers diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 20241b1b3..625debd24 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -24,6 +24,7 @@ dependencies = [ "opentelemetry-api>=1.34.1", "opentelemetry-sdk>=1.34.1", "opentelemetry-exporter-otlp-proto-http>=1.34.1", + "truststore>=0.10.0", ] diff --git a/backend/tests/app/test_main_truststore.py b/backend/tests/app/test_main_truststore.py new file mode 100644 index 000000000..dfa97aa8c --- /dev/null +++ b/backend/tests/app/test_main_truststore.py @@ -0,0 +1,50 @@ +import os +import subprocess +import sys +import textwrap +from pathlib import Path + +import pytest + + +@pytest.mark.unit +def test_main_import_enables_system_trust_store_for_httpx(tmp_path): + """Guard the backend entrypoint's early truststore injection.""" + backend_dir = Path(__file__).resolve().parents[2] + script = textwrap.dedent( + """ + import ssl + + import httpx + import httpx._config + import truststore + + import main + + if ssl.SSLContext is not truststore.SSLContext: + raise SystemExit( + f"ssl.SSLContext was not patched: {ssl.SSLContext!r}" + ) + + ctx = httpx._config.create_ssl_context() + if not isinstance(ctx, truststore.SSLContext): + raise SystemExit( + "httpx did not use truststore after backend startup: " + f"{type(ctx)!r}" + ) + """ + ) + env = os.environ.copy() + env["HOME"] = str(tmp_path) + env["USERPROFILE"] = str(tmp_path) + + result = subprocess.run( + [sys.executable, "-c", script], + cwd=backend_dir, + env=env, + text=True, + capture_output=True, + timeout=30, + ) + + assert result.returncode == 0, result.stdout + result.stderr diff --git a/backend/uv.lock b/backend/uv.lock index 365db8454..c26df7445 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -230,6 +230,7 @@ dependencies = [ { name = "pydash" }, { name = "python-dotenv" }, { name = "qdrant-client" }, + { name = "truststore" }, { name = "uvicorn", extra = ["standard"] }, ] @@ -261,6 +262,7 @@ requires-dist = [ { name = "pydash", specifier = ">=8.0.5" }, { name = "python-dotenv", specifier = ">=1.1.0" }, { name = "qdrant-client", specifier = ">=1.16.2" }, + { name = "truststore", specifier = ">=0.10.0" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.34.2" }, ] @@ -2680,6 +2682,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, ] +[[package]] +name = "truststore" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.730Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.460Z" }, +] + [[package]] name = "typer" version = "0.25.0"