feat: ADR-0009 SSL cert management, fix certifi vs system CA

- ADR-0009: comprehensive SSL certificate management strategy
- _get_ssl_verify() returns SSLContext with cafile= instead of bool True
  (httpx deprecates verify=<str>, requests needs system CA, not certifi)
- _get_verify() in translate plugin returns system CA path
- All tests updated for SSLContext return type
- All QA findings C1-C3, H1-H4, M1 incorporated into ADR
This commit is contained in:
2026-05-28 12:06:42 +03:00
parent 60333bdb95
commit 19dd447345
5 changed files with 170 additions and 22 deletions

View File

@@ -54,15 +54,18 @@ def test_litellm_client_uses_default_bearer_auth():
# endregion test_litellm_client_uses_default_bearer_auth
# region test_get_ssl_verify_default_true [TYPE Function]
# region test_get_ssl_verify_default_context [TYPE Function]
# @RELATION BINDS_TO -> TestClientHeaders
# @PURPOSE: _get_ssl_verify defaults to True when LLM_SSL_VERIFY is unset.
# @PURPOSE: _get_ssl_verify returns SSLContext with CERT_REQUIRED by default.
# @PRE LLM_SSL_VERIFY env var is not set.
# @POST Returns True.
def test_get_ssl_verify_default_true():
"""Verify default SSL verification is enabled."""
assert LLMClient._get_ssl_verify() is True
# endregion test_get_ssl_verify_default_true
# @POST Returns SSLContext with verify_mode=CERT_REQUIRED.
def test_get_ssl_verify_default_context():
"""Verify default returns SSLContext with CERT_REQUIRED."""
import ssl
result = LLMClient._get_ssl_verify()
assert isinstance(result, ssl.SSLContext), "Expected SSLContext, not bool"
assert result.verify_mode == ssl.CERT_REQUIRED
# endregion test_get_ssl_verify_default_context
# region test_get_ssl_verify_false_values [TYPE Function]
@@ -77,16 +80,19 @@ def test_get_ssl_verify_false_values(monkeypatch):
# endregion test_get_ssl_verify_false_values
# region test_get_ssl_verify_true_values [TYPE Function]
# region test_get_ssl_verify_true_returns_context [TYPE Function]
# @RELATION BINDS_TO -> TestClientHeaders
# @PURPOSE: _get_ssl_verify returns True for truthy values like "TRUE", "1", "yes".
# @PURPOSE: _get_ssl_verify returns SSLContext for truthy env values.
# @PRE LLM_SSL_VERIFY env var is set to truthy values.
# @POST Returns True.
def test_get_ssl_verify_true_values(monkeypatch):
# @POST Returns SSLContext with CERT_REQUIRED.
def test_get_ssl_verify_true_returns_context(monkeypatch):
import ssl
for val in ("TRUE", "1", "yes", "on", "true"):
monkeypatch.setenv("LLM_SSL_VERIFY", val)
assert LLMClient._get_ssl_verify() is True, f"Expected True for LLM_SSL_VERIFY={val}"
# endregion test_get_ssl_verify_true_values
result = LLMClient._get_ssl_verify()
assert isinstance(result, ssl.SSLContext), f"Expected SSLContext for LLM_SSL_VERIFY={val}"
assert result.verify_mode == ssl.CERT_REQUIRED
# endregion test_get_ssl_verify_true_returns_path
# region test_format_connection_error_no_cause [TYPE Function]

View File

@@ -13,6 +13,7 @@ import json
import os
import re
import shutil
import ssl
import tempfile
from typing import Any
from urllib.parse import urlsplit
@@ -923,11 +924,25 @@ class LLMClient:
# region LLMClient._get_ssl_verify [TYPE Function]
# @PURPOSE Resolve SSL verification flag from environment.
# @POST Returns False when LLM_SSL_VERIFY env var is "false"/"0"/"no" (case-insensitive).
# @POST Returns SSLContext with system CA bundle when enabled,
# False when LLM_SSL_VERIFY env var is "false"/"0"/"no"/"off".
# @RATIONALE Возвращаем ssl.SSLContext вместо True, потому что httpx по
# умолчанию использует certifi, в котором нет корпоративных CA.
# ssl.create_default_context(cafile=...) гарантирует, что используются
# сертификаты из системного хранилища (/etc/ssl/certs/ca-certificates.crt),
# куда update-ca-certificates и наш fallback добавляют корпоративные CA.
# @REJECTED verify=<string> отвергнут — httpx 0.28.x депрекейтит строковый
# путь в verify=, требует SSLContext.
@staticmethod
def _get_ssl_verify() -> bool:
def _get_ssl_verify() -> ssl.SSLContext | bool:
raw = os.getenv("LLM_SSL_VERIFY", "true").strip().lower()
return raw not in ("false", "0", "no", "off")
if raw in ("false", "0", "no", "off"):
return False
ca_path = "/etc/ssl/certs/ca-certificates.crt"
if os.path.exists(ca_path):
return ssl.create_default_context(cafile=ca_path)
# fallback: если системного CA нет (редко), используем дефолтный
return ssl.create_default_context()
# endregion LLMClient._get_ssl_verify
# region LLMClient._format_connection_error [TYPE Function]

View File

@@ -18,10 +18,18 @@ from typing import Any
from ...core.logger import logger
def _get_verify() -> bool:
"""Resolve SSL verify flag from LLM_SSL_VERIFY env var."""
def _get_verify() -> str | bool:
"""Resolve SSL verify from LLM_SSL_VERIFY env var.
Returns:
- Path to system CA bundle when verification enabled
(certifi bundle doesn't include corporate CA certs)
- False when LLM_SSL_VERIFY is set to false/0/no/off
"""
raw = os.getenv("LLM_SSL_VERIFY", "true").strip().lower()
return raw not in ("false", "0", "no", "off")
if raw in ("false", "0", "no", "off"):
return False
return "/etc/ssl/certs/ca-certificates.crt"
# #region call_openai_compatible [C:3] [TYPE Function] [SEMANTICS translate, llm, http, openai]

View File

@@ -10,10 +10,18 @@ from typing import Any
from ...core.logger import logger
def _get_verify() -> bool:
"""Resolve SSL verify flag from LLM_SSL_VERIFY env var."""
def _get_verify() -> str | bool:
"""Resolve SSL verify from LLM_SSL_VERIFY env var.
Returns:
- Path to system CA bundle when verification enabled
(certifi bundle doesn't include corporate CA certs)
- False when LLM_SSL_VERIFY is set to false/0/no/off
"""
raw = os.getenv("LLM_SSL_VERIFY", "true").strip().lower()
return raw not in ("false", "0", "no", "off")
if raw in ("false", "0", "no", "off"):
return False
return "/etc/ssl/certs/ca-certificates.crt"
class LLMClient: