Skip to content

[Fix] Networking - allow using CA Bundles #11906

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 52 additions & 14 deletions litellm/llms/custom_httpx/http_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,39 @@ def _should_use_aiohttp_transport() -> bool:
verbose_logger.debug("Using AiohttpTransport...")
return True

@staticmethod
def _get_ssl_connector_kwargs(
ssl_verify: Optional[bool] = None,
ssl_context: Optional[ssl.SSLContext] = None,
) -> Dict[str, Any]:
"""
Helper method to get SSL connector initialization arguments for aiohttp TCPConnector.

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
"""
connector_kwargs: Dict[str, Any] = {
"local_addr": ("0.0.0.0", 0) if litellm.force_ipv4 else None,
}

if ssl_context is not None:
# Priority 1: Use the provided custom SSL context
connector_kwargs["ssl"] = ssl_context
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

@staticmethod
def _create_aiohttp_transport(
ssl_verify: Optional[bool] = None,
Expand All @@ -541,29 +574,34 @@ def _create_aiohttp_transport(
"""
Creates an AiohttpTransport with RequestNotRead error handling

- If force_ipv4 is True, it will create an AiohttpTransport with local_addr set to "0.0.0.0"
- [Default] If force_ipv4 is False, it will create an AiohttpTransport with default settings
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

#########################################################
# If ssl_verify is None, set it to True
# TCP Connector does not allow ssl_verify to be None
# by default aiohttp sets ssl_verify to True
#########################################################
if ssl_verify is None:
ssl_verify = True
connector_kwargs = AsyncHTTPHandler._get_ssl_connector_kwargs(
ssl_verify=ssl_verify, ssl_context=ssl_context
)

verbose_logger.debug("Creating AiohttpTransport...")
return LiteLLMAiohttpTransport(
client=lambda: ClientSession(
connector=TCPConnector(
verify_ssl=ssl_verify,
ssl_context=ssl_context,
local_addr=("0.0.0.0", 0) if litellm.force_ipv4 else None,
)
connector=TCPConnector(**connector_kwargs)
),
)


@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]:
Expand Down
32 changes: 32 additions & 0 deletions tests/test_litellm/llms/custom_httpx/test_http_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import sys
from unittest.mock import MagicMock, patch

import certifi
import httpx
import pytest
from aiohttp import ClientSession, TCPConnector
Expand Down Expand Up @@ -120,3 +121,34 @@ async def test_ssl_verification_with_aiohttp_transport():

# assert both litellm transport and aiohttp session have ssl_verify=False
assert transport_connector._ssl == aiohttp_session.connector._ssl


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_context_integration():
"""Integration test that _get_ssl_context() returns a working SSL context"""
# Call the static method without mocking
ssl_context = AsyncHTTPHandler._get_ssl_context()

# Verify it returns an SSLContext instance
assert isinstance(ssl_context, ssl.SSLContext)

# Verify it has basic SSL context properties
assert ssl_context.protocol is not None
assert ssl_context.verify_mode is not None
Loading