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:
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user