Centralized SSL via one contract: CERTS_PATH=/opt/certs mounted into all containers.
Backend:
- NEW: backend/src/core/ssl.py — system_ssl_context(), httpx_verify(),
cert_dir_inventory()
- LLMClient._get_ssl_verify() → delegates to core.ssl
- _llm_async_http._get_verify() → delegates to core.ssl
- Removed LLM_SSL_VERIFY env reading from all runtime code
Docker:
- NEW: docker/certs.sh — shared cert installer (PEM/DER/cer to .crt conversion,
update-ca-certificates, hash symlinks, NSS import)
- NEW: docker/agent.entrypoint.sh — agent entrypoint with cert installation
- backend.entrypoint.sh → uses certs.sh instead of install_llm_ca_certs
- Dockerfile.agent → adds ca-certificates, openssl, entrypoint
Compose:
- Removed LLM_CA_CERT_URLS and LLM_SSL_VERIFY from all compose files
- Added CERTS_PATH volume mount to agent (dev + enterprise)
- Added certs volume mount to backend/agent in dev compose
Env examples:
- Removed LLM_SSL_VERIFY, LLM_CA_CERT_URLS from .env.example,
.env.enterprise-clean.example, .env.current.example, .env.master.example,
backend/.env.example
- Enhanced CERTS_PATH comments with accepted formats
Diagnostics:
- diag_container.py: removed LLM_* checks, added CERTS_PATH inventory,
uses core.ssl for context creation
Tests:
- Updated test_llm_analysis_service, test_llm_async_http,
test_client_headers to verify centralized ssl context (no env disable)
- 4/4 SSL tests pass
16 KiB
Plan: centralized SSL certificate management for all containers
Goal
Replace scattered LLM-specific SSL environment variables with one centralized certificate/trust mechanism used consistently by backend, frontend, agent, Python HTTP clients, Playwright/Chromium, curl/openssl diagnostics, and release bundles.
User intent:
- Remove
LLM_CA_CERT_URLSandLLM_SSL_VERIFYfrom operator-facing configuration. - Do not manage LLM TLS separately from other corporate TLS needs.
- Certificates should be mounted/installed once through a single
CERTS_PATH//opt/certscontract. - All containers should trust the same corporate CA set.
- Runtime clients should use system trust, not custom per-client env toggles.
Current state inventory
ADR
docs/adr/ADR-0009-ssl-certificate-management.md- Correctly identified that OpenSSL 3.x works with
capath=/etc/ssl/certs/and can fail with flatcafilebundles. - Still documents
LLM_SSL_VERIFYandLLM_CA_CERT_URLSas separate LLM-specific paths. - Needs update: centralized
CERTS_PATHreplaces LLM-specific env vars.
- Correctly identified that OpenSSL 3.x works with
Backend container
docker/backend.entrypoint.shinstall_certificates()already installs*.crt/*.pemfrom${CERTS_PATH:-/opt/certs}into/usr/local/share/ca-certificates/custom, thenupdate-ca-certificates --fresh.install_llm_ca_certs()separately usesLLM_CA_CERT_URLSto download DER/PEM CA certs into/usr/local/share/ca-certificates/llm, then creates hash symlinks.install_ca_to_nss()imports custom and llm certs into Chromium NSS DB.- Problem: there are two certificate sources (
CERTS_PATHandLLM_CA_CERT_URLS) and only the LLM path has robust DER conversion/hash-symlink validation.
Frontend container
docker/frontend.entrypoint.sh- Uses
${CERTS_PATH:-/opt/certs}and installs mounted CA files into Alpine CA store. - Skips
server.crtandserver.key. - Does not use
LLM_CA_CERT_URLS/LLM_SSL_VERIFY, which is good.
- Uses
Agent container
docker/Dockerfile.agent- Python slim image currently installs
libgl1 libglib2.0-0 libpq5, but not necessarilyca-certificates,openssl, or a startup entrypoint to install/opt/certsCAs. - Compose mounts
CERTS_PATHinto/opt/certsbut agent likely does not install those certificates into trust store. - If agent makes HTTPS calls to backend/LLM/internal APIs, it must share the same trust installation.
- Python slim image currently installs
Python clients
-
backend/src/plugins/llm_analysis/service.pyLLMClient._get_ssl_verify()readsLLM_SSL_VERIFY; if false, returnsFalse; otherwise returnsssl.create_default_context(capath="/etc/ssl/certs").- Need central replacement: no LLM-specific toggle; always use centralized trust context.
-
backend/src/plugins/translate/_llm_async_http.py_get_verify()readsLLM_SSL_VERIFY; if false, disables TLS verification; otherwise usesssl.create_default_context(capath="/etc/ssl/certs").- Need central replacement.
-
Other LLM/provider/test code may use
httpxorAsyncOpenAI; all should route through a single helper.
Compose/env examples
Likely references to remove/update:
docker-compose.ymldocker-compose.enterprise-clean.ymldocker-compose.e2e.ymlbuild.shgenerated bundle compose.env.example.env.enterprise-clean.examplebackend/.env.exampledocker/.env.agent.example.env.current.example,.env.master.example,.env.e2e.example,frontend/.env.examplescripts/diag_container.py
Current operator-facing variables:
- Keep:
CERTS_PATH=./certs - Remove:
LLM_CA_CERT_URLS - Remove:
LLM_SSL_VERIFY
Proposed canonical contract
One certificate source
CERTS_PATH is the only external certificate input.
Host layout:
./certs/
RUSAL_ROOT.crt
RGM_Issuing.crt
lite_ai_issuing.crt
any-other-corporate-ca.crt
server.crt # optional frontend HTTPS server cert, not trusted as CA
server.key # optional frontend HTTPS server key, not trusted as CA
server.p12 # optional frontend HTTPS bundle
Container mount:
volumes:
- ${CERTS_PATH:-./certs}:/opt/certs:ro
Rules:
.crt,.pem,.cer,.derunder/opt/certsare treated as trust candidates.server.crt,server.key,server.p12,*.key,*.p12,*.pfxare not imported as CA trust anchors.- DER certificates are detected and converted to PEM.
- Every valid CA cert is installed into system CA store.
- Backend additionally imports valid CA certs into NSS DB for Chromium/Playwright.
- No runtime download of certificates from URLs.
- No environment variable disables TLS verification.
One Python SSL helper
Add central helper, e.g. backend/src/core/ssl.py:
get_ssl_context() -> ssl.SSLContext- returns
ssl.create_default_context(capath="/etc/ssl/certs")if available - fallback to
ssl.create_default_context()only if capath missing
- returns
get_httpx_verify() -> ssl.SSLContext- optional
get_requests_verify() -> str | bool- if requests is still used, use
/etc/ssl/certs/ca-certificates.crtonly if unavoidable; prefer no requests-specific LLM client.
- if requests is still used, use
- no
verify=Falsecode path. - log only safe diagnostic:
SSLContext(capath=/etc/ssl/certs).
Replace local _get_ssl_verify() / _get_verify() functions with the shared helper.
One diagnostics script
Update scripts/diag_container.py:
- Remove
LLM_SSL_VERIFYandLLM_CA_CERT_URLSchecks. - Report only:
CERTS_PATHvalue (if set)/opt/certscontents- system CA store state
- hash symlinks
- NSS DB entries
openssl -CApath- Python
SSLContext(capath) httpx(verify=context)- encryption health
- If cert for target fails, say:
- “Place the issuing/root CA
.crt/.cer/.derintoCERTS_PATHand restart containers.”
- “Place the issuing/root CA
Implementation plan
1. Backend entrypoint: unify certificate installation
File: docker/backend.entrypoint.sh
Changes:
- Replace/extend
install_certificates()to become the single robust installer. - Remove
install_llm_ca_certs()or leave as unused deprecated internal no-op during one release. - Add DER/PEM auto-detection for all certs from
/opt/certs:- Try
openssl x509 -in file -nooutas PEM. - If fails, try
openssl x509 -inform DER -in file -out converted.crt.
- Try
- Copy normalized certs to
/usr/local/share/ca-certificates/custom/. - Exclude non-CA/server/private files:
server.crt,server.key,server.p12,*.key,*.p12,*.pfx.
- Run
update-ca-certificates --freshonce. - Validate each installed cert:
- fingerprint presence in
ca-certificates.crtif possible - hash symlink exists under
/etc/ssl/certs/<hash>.N - create collision-safe symlink if missing
- fingerprint presence in
- Import the same normalized certs to NSS DB.
- Emit clear startup logs:
- installed count
- skipped count
- invalid cert count
- hash symlink count
- NSS import count
Expected result:
- Adding
lite_ai_issuing.crtto./certsis enough; no LLM-specific URL env var.
2. Frontend entrypoint: align cert parser
File: docker/frontend.entrypoint.sh
Changes:
- Keep
CERTS_PATHas only input. - Accept
.crt,.pem,.cer,.der. - Convert DER to PEM before
update-ca-certificates. - Keep skipping server cert/key/bundle files.
- Log installed/skipped/invalid certs.
Expected result:
- Frontend/nginx Alpine trust store uses the same
./certscontent.
3. Agent container: add centralized cert installation
Files:
docker/Dockerfile.agent- new
docker/agent.entrypoint.shor reuse a shared cert installer copied into agent image.
Changes:
- Install system packages:
ca-certificatesopenssl
- Add entrypoint that runs the same centralized cert installer against
/opt/certsbeforepython -m src.agent.run. - Ensure compose mounts
${CERTS_PATH:-./certs}:/opt/certs:rofor agent. - Use the same skip rules and DER conversion.
Expected result:
- Agent trusts corporate CA certs identically to backend.
4. Optional shared shell library
To avoid three divergent installers, create one shared script:
docker/certs.sh
Functions:
install_certs_debian()install_certs_alpine()normalize_cert_dir()install_to_nss()create_hash_symlinks()
Then:
- backend entrypoint sources
docker/certs.sh - frontend entrypoint sources
docker/certs.sh - agent entrypoint sources
docker/certs.sh
If minimizing churn, duplicate logic initially but prefer shared script for zero drift.
Recommended: shared docker/certs.sh.
5. Central Python SSL helper
New file:
backend/src/core/ssl.py
API:
def get_system_ssl_context() -> ssl.SSLContext:
...
def describe_ssl_context(ctx: ssl.SSLContext) -> str:
...
Update callers:
backend/src/plugins/llm_analysis/service.py- remove
LLM_SSL_VERIFYlogic - use
get_system_ssl_context()
- remove
backend/src/plugins/translate/_llm_async_http.py- remove
LLM_SSL_VERIFYlogic - use
get_system_ssl_context()
- remove
- search all
LLM_SSL_VERIFYoccurrences and remove from runtime code.
Complete files list needing changes (runtime + tests + docs):
| File | Action |
|---|---|
backend/src/core/ssl.py |
NEW — centralized SSL helper |
backend/src/plugins/llm_analysis/service.py |
Remove _get_ssl_verify, delegate to core.ssl |
backend/src/plugins/translate/_llm_async_http.py |
Remove _get_verify, delegate to core.ssl |
docker/backend.entrypoint.sh |
Remove install_llm_ca_certs, merge DER/PEM logic into unified installer |
docker/frontend.entrypoint.sh |
Add DER conversion, align with unified logic |
docker/Dockerfile.agent |
Add ca-certificates, openssl |
docker/agent.entrypoint.sh |
NEW — agent entrypoint with cert install |
docker/certs.sh |
NEW — shared cert installer (optional, refactor step) |
docker-compose.yml |
Remove LLM_SSL_VERIFY, LLM_CA_CERT_URLS; add CERTS_PATH mount |
docker-compose.enterprise-clean.yml |
Remove LLM_SSL_VERIFY, LLM_CA_CERT_URLS; add agent CERTS_PATH mount |
docker-compose.e2e.yml |
Add CERTS_PATH |
build.sh |
Update generated compose |
.env.example |
Remove LLM_SSL_VERIFY, LLM_CA_CERT_URLS; enhance CERTS_PATH comments |
.env.enterprise-clean.example |
Same |
.env.current.example |
Same |
.env.master.example |
Same |
backend/.env.example |
Same |
docker/.env.agent.example |
Same |
scripts/diag_container.py |
Remove LLM_* refs, add /opt/certs inventory |
scripts/check_llm_certs.py |
Remove LLM_SSL_VERIFY section (or deprecate file) |
docs/adr/ADR-0009-ssl-certificate-management.md |
Replace LLM_SSL_VERIFY + LLM_CA_CERT_URLS with centralized CERTS_PATH |
README.md |
Update cert section |
backend/tests/plugins/test_llm_analysis_service.py |
Update tests for centralized ssl helper |
backend/tests/plugins/translate/test_llm_async_http.py |
Same |
backend/tests/integration/test_superset_tls_custom_ca.py |
Same |
Policy:
- There is no
verify=Falseenv escape hatch. - If operators need temporary bypass for manual debugging, they can use curl/openssl outside app; app remains secure-by-default.
6. Compose/env cleanup
Files:
docker-compose.ymldocker-compose.enterprise-clean.ymldocker-compose.e2e.ymlbuild.shgenerated compose.env.example.env.enterprise-clean.examplebackend/.env.exampledocker/.env.agent.example- other
.env.*.example
Changes:
- Remove
LLM_CA_CERT_URLSfrom all compose env blocks and examples. - Remove
LLM_SSL_VERIFYfrom all compose env blocks and examples. - Keep one variable:
CERTS_PATH=./certs
- Add comments:
# Put all corporate root/intermediate CA certificates here.
# Applies to backend, frontend, and agent containers.
# Accepted trust files: *.crt, *.pem, *.cer, *.der
# Do not put private keys here except server.key/server.p12 used by frontend TLS.
CERTS_PATH=./certs
- Bundle generated compose must mount
CERTS_PATHinto all containers:- backend
- frontend
- agent
7. Diagnostics update
File: scripts/diag_container.py
Changes:
- Remove
LLM_SSL_VERIFYandLLM_CA_CERT_URLSreporting. - Add
/opt/certsinventory:- list recognized trust candidates
- list skipped server/private files
- list invalid files
- Add NSS DB diagnostics if
certutilis installed. - Fix OpenSSL output classification:
- if return code is 0 but no verify code parsed, print raw verify line excerpt.
- Summary should say:
If CApath/httpx failures:
-> put the issuing/root CA for target into CERTS_PATH (./certs)
-> restart affected containers
-> rerun this diagnostic
8. ADR update
File: docs/adr/ADR-0009-ssl-certificate-management.md
Changes:
- Replace “Layer 4: LLM_SSL_VERIFY Escape Hatch” with “Layer 4: centralized CERTS_PATH trust contract”.
- Mark old env vars as removed/deprecated:
LLM_SSL_VERIFYremovedLLM_CA_CERT_URLSremoved
- Update key files table.
- Update diagnostics/runbook.
- State policy:
- application code never disables TLS verification via env var
- all trust anchors come from mounted
CERTS_PATH
9. Tests
Backend unit tests:
- New tests for
backend/src/core/ssl.py:- returns
SSLContext - prefers
capath=/etc/ssl/certswhen present - does not read
LLM_SSL_VERIFY - cannot return
False
- returns
Shell/script tests, if existing harness supports:
- Cert normalization:
- PEM
.crtaccepted .pemaccepted- DER
.ceraccepted/converted server.key,server.p12skipped- invalid file skipped with warning
- PEM
Integration/smoke:
- Start backend with certs mounted.
- Run:
python3 /tmp/diag_container.py --target lite.ai.rusal.com:443
Expected after correct CA placed in ./certs:
- OpenSSL capath OK
- Python SSLContext OK
- httpx OK
10. Migration/operator steps
For production operators:
- Remove from
.env.enterprise-clean:
LLM_SSL_VERIFY=...
LLM_CA_CERT_URLS=...
- Put all corporate CA files under
./certs:
./certs/RUSAL_ROOT.crt
./certs/RGM_Issuing.crt
./certs/lite_ai_issuing.crt
- Restart all containers:
docker compose --env-file .env.enterprise-clean -f docker-compose.enterprise-clean.yml up -d --force-recreate
- Run diagnostics:
docker cp scripts/diag_container.py ss_tools-backend-1:/tmp/
docker compose exec backend python3 /tmp/diag_container.py --target lite.ai.rusal.com:443
- Verify expected:
openssl capath: OK
Python SSLContext(capath): OK
httpx(capath): OK
Acceptance criteria
- No runtime code reads
LLM_SSL_VERIFY. - No compose/env example exposes
LLM_SSL_VERIFYorLLM_CA_CERT_URLS. - Backend, frontend, and agent all mount
CERTS_PATHand install certs into their system trust stores. - Backend imports the same trust certs into NSS for Chromium/Playwright.
- Python LLM clients use one central SSL helper and never return
verify=Falsefrom env config. - Diagnostic script reports centralized
CERTS_PATHtrust state and no longer references LLM-specific env vars. - ADR-0009 reflects the new centralized design.
- Existing auth/encryption/key recovery tests continue passing.
Risks and mitigations
-
Risk: Removing
LLM_SSL_VERIFY=falseremoves an easy emergency bypass.- Mitigation: keep only a code-local debug override unavailable in compose/examples? Recommended: no runtime bypass; rely on correct CA installation.
-
Risk: Operators currently rely on
LLM_CA_CERT_URLSfor PKI downloads.- Mitigation: document how to download/copy CA files into
./certs; do not download at runtime.
- Mitigation: document how to download/copy CA files into
-
Risk: Agent previously did not install CAs.
- Mitigation: add agent entrypoint and smoke-test HTTPS from inside agent.
-
Risk: Frontend
server.crtmay accidentally be imported as CA.- Mitigation: explicit skip list for server/private files across all installers.
Open question
Should we completely remove LLM_SSL_VERIFY support from code, or keep a hidden ALLOW_INSECURE_SSL=false emergency variable that is not documented or present in compose/examples? Recommended: completely remove SSL bypass from application runtime.