fix(ssl+agent): capath for all HTTP clients + isolate gradio import
SSL fix (ADR-0009 Finding 7): - Replace ssl.create_default_context() with system_ssl_context(capath) in client_registry.py, async_network.py, notifications/providers.py, git/_base.py. Previous fix (0.5.1) only covered LLM clients; the Superset API client still used ssl.create_default_context() which loads cafile (flat bundle) where OpenSSL 3.x ignores intermediate CA certificates. system_ssl_context() uses capath only (hash symlinks). Agent fix: - Extract _check_llm_provider_health + _llm_status from agent/app.py into agent/_llm_health.py. The /api/agent/llm-status endpoint was importing from agent/app.py which triggers 'import gradio' at module level. Backend container does not have gradio installed, causing ModuleNotFoundError (500 error) every 30s on health check polling. Config: - Add ALLOWED_ORIGINS to docker-compose.yml + docker-compose.enterprise-clean.yml ADR-0009 updated: Layer 2 table expanded with 4 missing clients, Finding 5 reconciled (LLM_CA_CERT_URLS restored in 0.5.1), version 0.5.2. Verified: 1110 unit tests passed, gradio import isolation confirmed.
This commit is contained in:
@@ -10,6 +10,10 @@
|
||||
# @RELATION CALLS -> [backend/src/core/ssl.py]
|
||||
# @RELATION CALLS -> [backend/src/plugins/llm_analysis/service.py]
|
||||
# @RELATION CALLS -> [backend/src/plugins/translate/_llm_async_http.py]
|
||||
# @RELATION CALLS -> [backend/src/core/utils/client_registry.py]
|
||||
# @RELATION CALLS -> [backend/src/core/utils/async_network.py]
|
||||
# @RELATION CALLS -> [backend/src/services/notifications/providers.py]
|
||||
# @RELATION CALLS -> [backend/src/services/git/_base.py]
|
||||
# @RELATION CALLS -> [scripts/check_llm_certs.py]
|
||||
# @RELATION CALLS -> [scripts/diag_container.py]
|
||||
# @RATIONALE superset-tools operates in corporate environments with internal Certificate Authorities.
|
||||
@@ -81,12 +85,19 @@ Diagnostic matrix (verified on production server 2026-05-28):
|
||||
|
||||
| Library | File | Mechanism | Status |
|
||||
|---------|------|-----------|--------|
|
||||
| `httpx` (AsyncOpenAI) | `service.py:LLMClient._get_ssl_verify()` | `ssl.create_default_context(capath="/etc/ssl/certs/")` | ✅ Works (0.1.7) |
|
||||
| `requests` (`_llm_http.py`) | `_get_verify()` | `"/etc/ssl/certs/"` (string path to dir) | ✅ Works (0.1.7) |
|
||||
| `requests` (`preview_llm_client.py`) | `_get_verify()` | `"/etc/ssl/certs/"` (string path to dir) | ✅ Works (0.1.7) |
|
||||
| `httpx` (AsyncOpenAI) | `service.py:LLMClient._get_ssl_verify()` | `system_ssl_context(capath="/etc/ssl/certs/")` | ✅ Fixed (0.5.1) |
|
||||
| `httpx` (translate) | `_llm_async_http.py:_get_verify()` | `httpx_verify()` → `system_ssl_context(capath)` | ✅ Fixed (0.5.1) |
|
||||
| `httpx` (SupersetClientRegistry) | `client_registry.py:get_client()` | `system_ssl_context(capath="/etc/ssl/certs/")` | ✅ Fixed (0.5.2) |
|
||||
| `httpx` (AsyncAPIClient) | `async_network.py:__init__()` | `system_ssl_context(capath="/etc/ssl/certs/")` | ✅ Fixed (0.5.2) |
|
||||
| `httpx` (Notifications) | `notifications/providers.py:_get_http_client()` | `system_ssl_context(capath="/etc/ssl/certs/")` | ✅ Fixed (0.5.2) |
|
||||
| `httpx` (Git) | `git/_base.py:GitServiceBase.__init__()` | `system_ssl_context(capath="/etc/ssl/certs/")` | ✅ Fixed (0.5.2) |
|
||||
|
||||
Key insight: `verify=True` uses certifi, NOT system CA. `verify=<cafile_path>` ignores
|
||||
intermediate CA in OpenSSL 3.x. `verify=<capath_dir>` works correctly.
|
||||
`ssl.create_default_context()` without explicit capath loads BOTH cafile and capath
|
||||
from OpenSSL defaults — the cafile may fail intermediate CA chain-building (Finding 7).
|
||||
`system_ssl_context(capath="/etc/ssl/certs")` loads ONLY capath (hash symlinks),
|
||||
correctly building chains with intermediate CAs.
|
||||
|
||||
### Layer 3: NSS Database (Playwright Chromium)
|
||||
|
||||
@@ -126,6 +137,10 @@ intermediate CA in OpenSSL 3.x. `verify=<capath_dir>` works correctly.
|
||||
| `docker/frontend.entrypoint.sh` | Installs CA certs from `CERTS_PATH` into Alpine store |
|
||||
| `docker/agent.entrypoint.sh` | Sources `certs.sh`, installs certs for agent container |
|
||||
| `backend/src/core/ssl.py` | Centralized SSL helper: `system_ssl_context()`, `httpx_verify()`, `describe_context()` |
|
||||
| `backend/src/core/utils/client_registry.py` | SupersetClientRegistry: `get_client()` → `system_ssl_context()` |
|
||||
| `backend/src/core/utils/async_network.py` | AsyncAPIClient: `__init__()` → `system_ssl_context()` |
|
||||
| `backend/src/services/notifications/providers.py` | Notification HTTP: `_get_http_client()` → `system_ssl_context()` |
|
||||
| `backend/src/services/git/_base.py` | Git HTTP: `GitServiceBase.__init__()` → `system_ssl_context()` |
|
||||
| `plugins/llm_analysis/service.py` | `LLMClient._get_ssl_verify()` → delegates to `core.ssl.httpx_verify()` |
|
||||
| `plugins/translate/_llm_async_http.py` | `_get_verify()` → delegates to `core.ssl.httpx_verify()` |
|
||||
| `scripts/check_llm_certs.py` | Full diagnostic: openssl, httpx, requests, NSS, centralized SSL |
|
||||
@@ -196,11 +211,13 @@ Corporate PKI servers (`pki.rusal.com`) often serve certificates in DER (binary)
|
||||
`update-ca-certificates` requires PEM. Auto-detection and conversion
|
||||
(`openssl x509 -inform DER -outform PEM`) is essential.
|
||||
|
||||
### Finding 5: Chicken-and-egg TLS bootstrap (HISTORICAL — removed in 0.2.x)
|
||||
### Finding 5: Chicken-and-egg TLS bootstrap (HISTORICAL — resolved in 0.5.1)
|
||||
Downloading a CA certificate from a PKI server that uses the same CA causes a TLS
|
||||
verification loop. Formerly handled by `install_llm_ca_certs()` with `curl --insecure`.
|
||||
**Removed in 0.2.x**: certificates must be placed in `CERTS_PATH` volume mount by operator,
|
||||
eliminating the chicken-and-egg download problem.
|
||||
**Removed in 0.2.x**: certificates must be placed in `CERTS_PATH` volume mount by operator.
|
||||
**Restored in 0.5.1**: `LLM_CA_CERT_URLS` HTTP download re-added with `--proto '=http'`
|
||||
policy (plain HTTP only, no TLS chicken-and-egg). Both channels (`LLM_CA_CERT_URLS`
|
||||
download + `CERTS_PATH` mount) are now active simultaneously.
|
||||
|
||||
### Finding 6: NSS DB format
|
||||
Chromium uses NSS Shared DB in `~/.pki/nssdb/`. The `sql:` prefix is required for
|
||||
@@ -233,9 +250,9 @@ Fix: pass `input=""` or `echo | openssl s_client ...`.
|
||||
xz -dc superset-tools-backend.0.2.x.tar.xz | docker load
|
||||
xz -dc superset-tools-frontend.0.2.x.tar.xz | docker load
|
||||
|
||||
# Place corporate CA files in ./certs (LLM_SSL_VERIFY and LLM_CA_CERT_URLS removed)
|
||||
# Place corporate CA files in ./certs (LLM_SSL_VERIFY removed, LLM_CA_CERT_URLS optional)
|
||||
ls ./certs/
|
||||
# RUSAL_ROOT.crt RGM_Issuing.crt etc.
|
||||
# RUSAL_ROOT.crt RGM_Issuing.crt UC_RUSAL_Policy_CA.crt etc.
|
||||
|
||||
docker compose -f docker-compose.enterprise-clean.yml \
|
||||
--env-file .env.enterprise-clean down
|
||||
@@ -259,5 +276,6 @@ docker compose -f docker-compose.enterprise-clean.yml \
|
||||
| 0.1.7 | **capath instead of cafile**: OpenSSL 3.x cafile limitation discovered and fixed. `.crt` extension for downloaded certs. Diagnostic script rewritten. |
|
||||
| 0.2.x | **Centralized SSL**: `LLM_SSL_VERIFY` and `LLM_CA_CERT_URLS` removed. Shared `docker/certs.sh` for all containers. `backend/src/core/ssl.py` central helper. `CERTS_PATH` volume mount is the only certificate input. Verify=False escape hatch permanently removed. |
|
||||
| 0.5.1 | **Restored**: `LLM_CA_CERT_URLS` HTTP download as primary cert input channel. `download_llm_ca_certs()` added to `certs.sh`. Downloaded certs saved to `llm/` subdirectory, `install_all_certs` extended to create hash symlinks for both `custom/` and `llm/` dirs. `CERTS_PATH` volume mount kept as secondary channel. Integration test added for HTTP download flow. |
|
||||
| 0.5.2 | **Fixed capath for ALL HTTP clients**: `ssl.create_default_context()` (no capath, loads cafile+capath) replaced with `system_ssl_context(capath="/etc/ssl/certs")` (capath only) in `client_registry.py`, `async_network.py`, `notifications/providers.py`, `git/_base.py`. Previous fix (0.5.1) only covered LLM clients; Superset API, notification, and git clients still used `ssl.create_default_context()` which fails intermediate CA chain-building in OpenSSL 3.x (Finding 7). Production diagnosis: RUSAL cert chain Root→Policy CA→RGM Issuing→Server requires Policy CA as intermediate; without capath, OpenSSL ignores it in cafile. |
|
||||
|
||||
# [/DEF:ADR-0009:ADR]
|
||||
|
||||
Reference in New Issue
Block a user