diff --git a/docs/my-website/docs/guides/security_settings.md b/docs/my-website/docs/guides/security_settings.md
index 008e620c515..7995f6c3c9c 100644
--- a/docs/my-website/docs/guides/security_settings.md
+++ b/docs/my-website/docs/guides/security_settings.md
@@ -3,12 +3,43 @@ import TabItem from '@theme/TabItem';
# SSL, HTTP Proxy Security Settings
-If you're in an environment using an older TTS bundle, with an older encryption, follow this guide.
+If you're in an environment using an older TTS bundle, with an older encryption, follow this guide. By default
+LiteLLM uses the certifi CA bundle for SSL verification, which is compatible with most modern servers.
+ However, if you need to disable SSL verification or use a custom CA bundle, you can do so by following the steps below.
+Be aware that environmental variables take precedence over the settings in the SDK.
-LiteLLM uses HTTPX for network requests, unless otherwise specified.
+LiteLLM uses HTTPX for network requests, unless otherwise specified.
-## 1. Disable SSL verification
+## 1. Custom CA Bundle
+
+You can set a custom CA bundle file path using the `SSL_CERT_FILE` environmental variable or passing a string to the the ssl_verify setting.
+
+
+
+
+```python
+import litellm
+litellm.ssl_verify = "client.pem"
+```
+
+
+
+```yaml
+litellm_settings:
+ ssl_verify: "client.pem"
+```
+
+
+
+
+```bash
+export SSL_CERT_FILE="client.pem"
+```
+
+
+
+## 2. Disable SSL verification
@@ -35,14 +66,42 @@ export SSL_VERIFY="False"
-## 2. Lower security settings
+## 3. Lower security settings
+
+The `ssl_security_level` allows setting a lower security level for SSL connections.
+
+
+
+
+```python
+import litellm
+litellm.ssl_security_level = "DEFAULT@SECLEVEL=1"
+```
+
+
+
+```yaml
+litellm_settings:
+ ssl_security_level: "DEFAULT@SECLEVEL=1"
+```
+
+
+
+```bash
+export SSL_SECURITY_LEVEL="DEFAULT@SECLEVEL=1"
+```
+
+
+
+## 4. Certificate authentication
+
+The `SSL_CERTIFICATE` environmental variable or `ssl_certificate` attribute allows setting a client side certificate to authenticate the client to the server.
```python
import litellm
-litellm.ssl_security_level = 1
litellm.ssl_certificate = "/path/to/certificate.pem"
```
@@ -50,20 +109,18 @@ litellm.ssl_certificate = "/path/to/certificate.pem"
```yaml
litellm_settings:
- ssl_security_level: 1
ssl_certificate: "/path/to/certificate.pem"
```
```bash
-export SSL_SECURITY_LEVEL="1"
export SSL_CERTIFICATE="/path/to/certificate.pem"
```
-## 3. Use HTTP_PROXY environment variable
+## 5. Use HTTP_PROXY environment variable
Both httpx and aiohttp libraries use `urllib.request.getproxies` from environment variables. Before client initialization, you may set proxy (and optional SSL_CERT_FILE) by setting the environment variables:
diff --git a/litellm/__init__.py b/litellm/__init__.py
index 4223e46f7fa..d4ef336dc83 100644
--- a/litellm/__init__.py
+++ b/litellm/__init__.py
@@ -214,6 +214,7 @@ use_litellm_proxy: bool = (
)
use_client: bool = False
ssl_verify: Union[str, bool] = True
+ssl_security_level: Optional[str] = None
ssl_certificate: Optional[str] = None
disable_streaming_logging: bool = False
disable_token_counter: bool = False
diff --git a/litellm/llms/custom_httpx/http_handler.py b/litellm/llms/custom_httpx/http_handler.py
index 34968a63aee..201874603e9 100644
--- a/litellm/llms/custom_httpx/http_handler.py
+++ b/litellm/llms/custom_httpx/http_handler.py
@@ -5,6 +5,7 @@ import time
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Mapping, Optional, Union
import httpx
+import certifi
from aiohttp import ClientSession, TCPConnector
from httpx import USE_CLIENT_DEFAULT, AsyncHTTPTransport, HTTPTransport
from httpx._types import RequestFiles
@@ -39,6 +40,72 @@ headers = {
_DEFAULT_TIMEOUT = httpx.Timeout(timeout=5.0, connect=5.0)
+def get_ssl_configuration(ssl_verify: Optional[VerifyTypes] = None) -> Union[bool, str, ssl.SSLContext]:
+ """
+ Unified SSL configuration function that handles ssl_context and ssl_verify logic.
+
+ SSL Configuration Priority:
+ 1. If ssl_verify is provided -> is a SSL context use the custom SSL context
+ 2. If ssl_verify is False -> disable SSL verification (ssl=False)
+ 3. If ssl_verify is a string -> use it as a path to CA bundle file
+ 4. If SSL_CERT_FILE environment variable is set and exists -> use it as CA bundle file
+ 5. Else will use default SSL context with certifi CA bundle
+
+ If ssl_security_level is set, it will apply the security level to the SSL context.
+
+ Args:
+ ssl_verify: SSL verification setting. Can be:
+ - None: Use default from environment/litellm settings
+ - False: Disable SSL verification
+ - True: Enable SSL verification
+ - str: Path to CA bundle file
+
+ Returns:
+ Union[bool, str, ssl.SSLContext]: Appropriate SSL configuration
+ """
+ from litellm.secret_managers.main import str_to_bool
+
+ if isinstance(ssl_verify, ssl.SSLContext):
+ # If ssl_verify is already an SSLContext, return it directly
+ return ssl_verify
+
+ # Get ssl_verify from environment or litellm settings if not provided
+ if ssl_verify is None:
+ ssl_verify = os.getenv("SSL_VERIFY", litellm.ssl_verify)
+ ssl_verify_bool = str_to_bool(ssl_verify) if isinstance(ssl_verify, str) else ssl_verify
+ if ssl_verify_bool is not None:
+ ssl_verify = ssl_verify_bool
+
+ ssl_security_level = os.getenv("SSL_SECURITY_LEVEL", litellm.ssl_security_level)
+
+ cafile = None
+ if isinstance(ssl_verify, str) and os.path.exists(ssl_verify):
+ cafile = ssl_verify
+ if not cafile:
+ ssl_cert_file = os.getenv("SSL_CERT_FILE")
+ if ssl_cert_file and os.path.exists(ssl_cert_file):
+ cafile = ssl_cert_file
+ else:
+ cafile = certifi.where()
+
+ if ssl_verify is not False:
+ custom_ssl_context = ssl.create_default_context(
+ cafile=cafile
+ )
+ # If security level is set, apply it to the SSL context
+ if (
+ ssl_security_level
+ and isinstance(ssl_security_level, str)
+ ):
+ # Create a custom SSL context with reduced security level
+ custom_ssl_context.set_ciphers(ssl_security_level)
+
+ # Use our custom SSL context instead of the original ssl_verify value
+ return custom_ssl_context
+
+ return ssl_verify
+
+
def mask_sensitive_info(error_message):
# Find the start of the key parameter
if isinstance(error_message, str):
@@ -119,29 +186,8 @@ class AsyncHTTPHandler:
event_hooks: Optional[Mapping[str, List[Callable[..., Any]]]],
ssl_verify: Optional[VerifyTypes] = None,
) -> httpx.AsyncClient:
- # SSL certificates (a.k.a CA bundle) used to verify the identity of requested hosts.
- # /path/to/certificate.pem
- if ssl_verify is None:
- ssl_verify = os.getenv("SSL_VERIFY", litellm.ssl_verify)
-
- ssl_security_level = os.getenv("SSL_SECURITY_LEVEL")
-
- # If ssl_verify is not False and we need a lower security level
- if (
- not ssl_verify
- and ssl_security_level
- and isinstance(ssl_security_level, str)
- ):
- # Create a custom SSL context with reduced security level
- custom_ssl_context = ssl.create_default_context()
- custom_ssl_context.set_ciphers(ssl_security_level)
-
- # If ssl_verify is a path to a CA bundle, load it into our custom context
- if isinstance(ssl_verify, str) and os.path.exists(ssl_verify):
- custom_ssl_context.load_verify_locations(cafile=ssl_verify)
-
- # Use our custom SSL context instead of the original ssl_verify value
- ssl_verify = custom_ssl_context
+ # Get unified SSL configuration
+ ssl_config = get_ssl_configuration(ssl_verify)
# An SSL certificate used by the requested host to authenticate the client.
# /path/to/client.pem
@@ -152,8 +198,8 @@ class AsyncHTTPHandler:
# Create a client with a connection pool
transport = AsyncHTTPHandler._create_async_transport(
- ssl_context=ssl_verify if isinstance(ssl_verify, ssl.SSLContext) else None,
- ssl_verify=ssl_verify if isinstance(ssl_verify, bool) else None,
+ ssl_context=ssl_config if isinstance(ssl_config, ssl.SSLContext) else None,
+ ssl_verify=ssl_config if isinstance(ssl_config, bool) else None,
)
return httpx.AsyncClient(
@@ -164,7 +210,7 @@ class AsyncHTTPHandler:
max_connections=concurrent_limit,
max_keepalive_connections=concurrent_limit,
),
- verify=ssl_verify,
+ verify=ssl_config,
cert=cert,
headers=headers,
)
@@ -544,7 +590,6 @@ class AsyncHTTPHandler:
SSL Configuration Priority:
1. If ssl_context is provided -> use the custom SSL context
2. If ssl_verify is False -> disable SSL verification (ssl=False)
- 3. If ssl_verify is True/None -> use default SSL context with certifi CA bundle
Returns:
Dict with appropriate SSL configuration for TCPConnector
@@ -559,10 +604,6 @@ class AsyncHTTPHandler:
elif ssl_verify is False:
# Priority 2: Explicitly disable SSL verification
connector_kwargs["verify_ssl"] = False
- else:
- # Priority 3: Use our default SSL context with certifi CA bundle
- # This covers ssl_verify=True and ssl_verify=None cases
- connector_kwargs["ssl"] = AsyncHTTPHandler._get_ssl_context()
return connector_kwargs
@@ -577,7 +618,6 @@ class AsyncHTTPHandler:
Note: aiohttp TCPConnector ssl parameter accepts:
- SSLContext: custom SSL context
- False: disable SSL verification
- - True: use default SSL verification (equivalent to ssl.create_default_context())
"""
from litellm.llms.custom_httpx.aiohttp_transport import LiteLLMAiohttpTransport
from litellm.secret_managers.main import str_to_bool
@@ -600,17 +640,6 @@ class AsyncHTTPHandler:
trust_env=trust_env,
),
)
-
-
- @staticmethod
- def _get_ssl_context() -> ssl.SSLContext:
- """
- Get the SSL context for the AiohttpTransport
- """
- import certifi
- return ssl.create_default_context(
- cafile=certifi.where()
- )
@staticmethod
def _create_httpx_transport() -> Optional[AsyncHTTPTransport]:
@@ -637,11 +666,8 @@ class HTTPHandler:
if timeout is None:
timeout = _DEFAULT_TIMEOUT
- # SSL certificates (a.k.a CA bundle) used to verify the identity of requested hosts.
- # /path/to/certificate.pem
-
- if ssl_verify is None:
- ssl_verify = os.getenv("SSL_VERIFY", litellm.ssl_verify)
+ # Get unified SSL configuration
+ ssl_config = get_ssl_configuration(ssl_verify)
# An SSL certificate used by the requested host to authenticate the client.
# /path/to/client.pem
@@ -658,7 +684,7 @@ class HTTPHandler:
max_connections=concurrent_limit,
max_keepalive_connections=concurrent_limit,
),
- verify=ssl_verify,
+ verify=ssl_config,
cert=cert,
headers=headers,
)
diff --git a/litellm/llms/openai/common_utils.py b/litellm/llms/openai/common_utils.py
index 853efb78466..aa670df0531 100644
--- a/litellm/llms/openai/common_utils.py
+++ b/litellm/llms/openai/common_utils.py
@@ -4,6 +4,7 @@ Common helpers / utils across al OpenAI endpoints
import hashlib
import json
+import ssl
from typing import Any, Dict, List, Literal, Optional, Union
import httpx
@@ -15,6 +16,7 @@ from litellm.llms.base_llm.chat.transformation import BaseLLMException
from litellm.llms.custom_httpx.http_handler import (
_DEFAULT_TTL_FOR_HTTPX_CLIENTS,
AsyncHTTPHandler,
+ get_ssl_configuration,
)
@@ -196,10 +198,16 @@ class BaseOpenAILLM:
if litellm.aclient_session is not None:
return litellm.aclient_session
+ # Get unified SSL configuration
+ ssl_config = get_ssl_configuration()
+
return httpx.AsyncClient(
limits=httpx.Limits(max_connections=1000, max_keepalive_connections=100),
- verify=litellm.ssl_verify,
- transport=AsyncHTTPHandler._create_async_transport(),
+ verify=ssl_config,
+ transport=AsyncHTTPHandler._create_async_transport(
+ ssl_context=ssl_config if isinstance(ssl_config, ssl.SSLContext) else None,
+ ssl_verify=ssl_config if isinstance(ssl_config, bool) else None,
+ ),
follow_redirects=True,
)
@@ -207,8 +215,12 @@ class BaseOpenAILLM:
def _get_sync_http_client() -> Optional[httpx.Client]:
if litellm.client_session is not None:
return litellm.client_session
+
+ # Get unified SSL configuration
+ ssl_config = get_ssl_configuration()
+
return httpx.Client(
limits=httpx.Limits(max_connections=1000, max_keepalive_connections=100),
- verify=litellm.ssl_verify,
+ verify=ssl_config,
follow_redirects=True,
)
diff --git a/tests/test_litellm/llms/custom_httpx/test_http_handler.py b/tests/test_litellm/llms/custom_httpx/test_http_handler.py
index 6f66bf20a0a..a649e9b6b9c 100644
--- a/tests/test_litellm/llms/custom_httpx/test_http_handler.py
+++ b/tests/test_litellm/llms/custom_httpx/test_http_handler.py
@@ -15,34 +15,35 @@ sys.path.insert(
) # Adds the parent directory to the system path
import litellm
from litellm.llms.custom_httpx.aiohttp_transport import LiteLLMAiohttpTransport
-from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler
+from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, get_ssl_configuration
@pytest.mark.asyncio
async def test_ssl_security_level(monkeypatch):
- # Set environment variable for SSL security level
- monkeypatch.setenv("SSL_SECURITY_LEVEL", "DEFAULT@SECLEVEL=1")
+ with patch.dict(os.environ, clear=True):
+ # Set environment variable for SSL security level
+ monkeypatch.setenv("SSL_SECURITY_LEVEL", "DEFAULT@SECLEVEL=1")
- # Create async client with SSL verification disabled to isolate SSL context testing
- client = AsyncHTTPHandler(ssl_verify=False)
+ # Create async client with SSL verification disabled to isolate SSL context testing
+ client = AsyncHTTPHandler()
- # Get the transport (should be LiteLLMAiohttpTransport)
- transport = client.client._transport
+ # Get the transport (should be LiteLLMAiohttpTransport)
+ transport = client.client._transport
- # Get the aiohttp ClientSession
- client_session = transport._get_valid_client_session()
+ # Get the aiohttp ClientSession
+ client_session = transport._get_valid_client_session()
- # Get the connector from the session
- connector = client_session.connector
+ # Get the connector from the session
+ connector = client_session.connector
- # Get the SSL context from the connector
- ssl_context = connector._ssl
- print("ssl_context", ssl_context)
+ # Get the SSL context from the connector
+ ssl_context = connector._ssl
+ print("ssl_context", ssl_context)
- # Verify that the SSL context exists and has the correct cipher string
- assert isinstance(ssl_context, ssl.SSLContext)
- # Optionally, check the ciphers string if needed
- # assert "DEFAULT@SECLEVEL=1" in ssl_context.get_ciphers()
+ # Verify that the SSL context exists and has the correct cipher string
+ assert isinstance(ssl_context, ssl.SSLContext)
+ # Optionally, check the ciphers string if needed
+ # assert "DEFAULT@SECLEVEL=1" in ssl_context.get_ciphers()
@pytest.mark.asyncio
@@ -151,28 +152,30 @@ async def test_aiohttp_transport_trust_env_setting(monkeypatch):
assert client_session_with_false_env._trust_env == default_trust_env
-def test_get_ssl_context():
- """Test that _get_ssl_context() returns a proper SSL context with certifi CA bundle"""
- with patch('ssl.create_default_context') as mock_create_context:
- # Mock the return value
- mock_ssl_context = MagicMock(spec=ssl.SSLContext)
- mock_create_context.return_value = mock_ssl_context
-
- # Call the static method
- result = AsyncHTTPHandler._get_ssl_context()
-
- # Verify ssl.create_default_context was called with certifi's CA file
- expected_ca_file = certifi.where()
- mock_create_context.assert_called_once_with(cafile=expected_ca_file)
-
- # Verify it returns the mocked SSL context
- assert result == mock_ssl_context
+def test_get_ssl_configuration():
+ """Test that get_ssl_configuration() returns a proper SSL context with certifi CA bundle
+ when no environment variables are set."""
+ with patch.dict(os.environ, clear=True):
+ with patch('ssl.create_default_context') as mock_create_context:
+ # Mock the return value
+ mock_ssl_context = MagicMock(spec=ssl.SSLContext)
+ mock_create_context.return_value = mock_ssl_context
+
+ # Call the static method
+ result = get_ssl_configuration()
+
+ # Verify ssl.create_default_context was called with certifi's CA file
+ expected_ca_file = certifi.where()
+ mock_create_context.assert_called_once_with(cafile=expected_ca_file)
+
+ # Verify it returns the mocked SSL context
+ assert result == mock_ssl_context
-def test_get_ssl_context_integration():
+def test_get_ssl_configuration_integration():
"""Integration test that _get_ssl_context() returns a working SSL context"""
# Call the static method without mocking
- ssl_context = AsyncHTTPHandler._get_ssl_context()
+ ssl_context = get_ssl_configuration()
# Verify it returns an SSLContext instance
assert isinstance(ssl_context, ssl.SSLContext)