From bd24880ee5b46f6229b44b9a8d9a79c642630171 Mon Sep 17 00:00:00 2001 From: yucheng Date: Fri, 30 Oct 2026 18:03:09 -0700 Subject: [PATCH] fix(proxy): stop refusing TLS verification off in FIPS mode Skipping certificate verification is a security choice, not a FIPS one: the TLS connection still only uses algorithms the FIPS provider allows. --- litellm/llms/custom_httpx/http_handler.py | 15 ------- litellm/proxy/common_utils/fips.py | 13 +------ .../configuration/test_fips_mode_boot.py | 26 +------------ tests/unit/proxy/common_utils/test_fips.py | 39 ++++++------------- ...test_proxy_server_endpoints_and_startup.py | 20 ---------- 5 files changed, 16 insertions(+), 97 deletions(-) diff --git a/litellm/llms/custom_httpx/http_handler.py b/litellm/llms/custom_httpx/http_handler.py index 81cd624883b..0b64885df45 100644 --- a/litellm/llms/custom_httpx/http_handler.py +++ b/litellm/llms/custom_httpx/http_handler.py @@ -336,21 +336,6 @@ def _create_ssl_context( def get_ssl_verify( ssl_verify: bool | str | None = None, -) -> bool | str: - """resolve_ssl_verify, refusing disabled TLS verification under LITELLM_FIPS_MODE.""" - resolved: Final = resolve_ssl_verify(ssl_verify) - if resolved is not False: - return resolved - - from litellm.proxy.common_utils.fips import TLS_VERIFICATION_DISABLED, FipsModeError, is_fips_mode - - if is_fips_mode(): - raise FipsModeError(TLS_VERIFICATION_DISABLED) - return resolved - - -def resolve_ssl_verify( - ssl_verify: bool | str | None = None, ) -> bool | str: """ Common utility to resolve the SSL verification setting. diff --git a/litellm/proxy/common_utils/fips.py b/litellm/proxy/common_utils/fips.py index 2cbe154d2a1..155696ea70f 100644 --- a/litellm/proxy/common_utils/fips.py +++ b/litellm/proxy/common_utils/fips.py @@ -4,10 +4,6 @@ from typing import Final from litellm.secret_managers.main import str_to_bool FIPS_MODE_ENV_VAR: Final = "LITELLM_FIPS_MODE" -TLS_VERIFICATION_DISABLED: Final = ( - f"{FIPS_MODE_ENV_VAR} is on but TLS certificate verification is disabled by SSL_VERIFY, " - "litellm_settings.ssl_verify or a model's ssl_verify. Remove the override, or point ssl_verify at a CA bundle." -) class FipsModeError(Exception): @@ -52,18 +48,13 @@ def cryptography_enforces_fips() -> bool: def fips_mode_refusal() -> str | None: - """Why the proxy must not start: LITELLM_FIPS_MODE is not a boolean, or is on but TLS verification is off - or Python's or the cryptography library's OpenSSL does not enforce FIPS. None when it may start.""" + """Why the proxy must not start: LITELLM_FIPS_MODE is not a boolean, or is on but Python's or the + cryptography library's OpenSSL does not enforce FIPS. None when it may start.""" value: Final = os.getenv(FIPS_MODE_ENV_VAR) if value and str_to_bool(value) is None: return f"{FIPS_MODE_ENV_VAR}={value!r} is not a boolean. Set it to true or false, or unset it." if not is_fips_mode(): return None - - from litellm.llms.custom_httpx.http_handler import resolve_ssl_verify - - if resolve_ssl_verify() is False: - return TLS_VERIFICATION_DISABLED if not openssl_enforces_fips(): return ( f"{FIPS_MODE_ENV_VAR} is on but OpenSSL does not enforce FIPS. Run the proxy on an OpenSSL with " diff --git a/tests/integration/configuration/test_fips_mode_boot.py b/tests/integration/configuration/test_fips_mode_boot.py index 0a20d14fb19..e2d7975b252 100644 --- a/tests/integration/configuration/test_fips_mode_boot.py +++ b/tests/integration/configuration/test_fips_mode_boot.py @@ -6,10 +6,10 @@ from typing import Final import pytest from tests.integration._support.client import Gateway -from tests.integration._support.process import owned_proxy, refused_boot_log +from tests.integration._support.process import refused_boot_log -def _config(directory: Path, upstream_url: str, *, ssl_verify: bool = True) -> Path: +def _config(directory: Path, upstream_url: str) -> Path: config: Final = directory / "fips_boot.yaml" config.write_text( json.dumps( @@ -20,7 +20,6 @@ def _config(directory: Path, upstream_url: str, *, ssl_verify: bool = True) -> P "litellm_params": {"model": "openai/fips-model", "api_base": upstream_url, "api_key": "sk-x"}, } ], - "litellm_settings": {"ssl_verify": ssl_verify}, } ) ) @@ -36,29 +35,8 @@ def test_fips_mode_refuses_to_start_when_openssl_does_not_enforce_fips(gateway: assert "LITELLM_FIPS_MODE is on but OpenSSL does not enforce FIPS" in log, log -@pytest.mark.parametrize( - "overrides, ssl_verify", - [({"SSL_VERIFY": "false"}, True), ({}, False)], - ids=["ssl-verify-env", "litellm-settings"], -) -def test_fips_mode_refuses_to_start_with_tls_verification_disabled( - gateway: Gateway, tmp_path: Path, overrides: dict[str, str], ssl_verify: bool -) -> None: - config: Final = _config(tmp_path, gateway.upstream_url, ssl_verify=ssl_verify) - log: Final = refused_boot_log(gateway, tmp_path, {"LITELLM_FIPS_MODE": "true", **overrides}, config=config) - assert "LITELLM_FIPS_MODE is on but TLS certificate verification is disabled" in log, log - - def test_fips_mode_refuses_to_start_on_a_value_that_is_not_a_boolean(gateway: Gateway, tmp_path: Path) -> None: log: Final = refused_boot_log( gateway, tmp_path, {"LITELLM_FIPS_MODE": "enforced"}, config=_config(tmp_path, gateway.upstream_url) ) assert "LITELLM_FIPS_MODE='enforced' is not a boolean" in log, log - - -def test_fips_mode_off_starts_with_tls_verification_disabled(gateway: Gateway, tmp_path: Path) -> None: - config: Final = _config(tmp_path, gateway.upstream_url, ssl_verify=False) - with owned_proxy( - gateway, tmp_path, {"SSL_VERIFY": "false"}, config=config, remove_environment=("LITELLM_FIPS_MODE",) - ) as proxy: - assert proxy.client.get("/health/readiness").status_code == 200 diff --git a/tests/unit/proxy/common_utils/test_fips.py b/tests/unit/proxy/common_utils/test_fips.py index 596d45f88d1..a2afea25306 100644 --- a/tests/unit/proxy/common_utils/test_fips.py +++ b/tests/unit/proxy/common_utils/test_fips.py @@ -8,8 +8,6 @@ from cryptography.exceptions import InternalError from cryptography.hazmat.bindings._rust import openssl as rust_openssl from cryptography.hazmat.primitives import hashes -import litellm -from litellm.llms.custom_httpx.http_handler import get_ssl_verify from litellm.proxy.common_utils.fips import ( FipsModeError, cryptography_enforces_fips, @@ -89,25 +87,23 @@ def test_this_runners_cryptography_openssl_does_not_enforce_fips() -> None: @pytest.mark.parametrize( - "fips_mode, ssl_verify_env, ssl_verify_setting, openssl_fips, cryptography_fips, refusal", + "fips_mode, ssl_verify, openssl_fips, cryptography_fips, refusal", [ - (None, "false", False, False, False, None), - ("", "false", False, False, False, None), - ("false", "false", False, False, False, None), - ("true", None, True, True, True, None), - ("true", "false", True, True, True, "TLS certificate verification is disabled"), - ("true", None, False, True, True, "TLS certificate verification is disabled"), - ("true", None, True, False, True, "is on but OpenSSL does not enforce FIPS"), - ("true", None, True, True, False, "cryptography library's OpenSSL has no working FIPS provider"), - ("enforced", None, True, True, True, "LITELLM_FIPS_MODE='enforced' is not a boolean"), + (None, None, False, False, None), + ("", None, False, False, None), + ("false", None, False, False, None), + ("true", None, True, True, None), + ("true", "false", True, True, None), + ("true", None, False, True, "is on but OpenSSL does not enforce FIPS"), + ("true", None, True, False, "cryptography library's OpenSSL has no working FIPS provider"), + ("enforced", None, True, True, "LITELLM_FIPS_MODE='enforced' is not a boolean"), ], ids=[ "unset", "empty", "false", "fips", - "ssl-verify-env-off", - "ssl-verify-setting-off", + "fips-with-tls-verification-off", "openssl-not-fips", "cryptography-not-fips", "not-a-boolean", @@ -116,29 +112,18 @@ def test_this_runners_cryptography_openssl_does_not_enforce_fips() -> None: def test_enforce_fips_mode( monkeypatch: pytest.MonkeyPatch, fips_mode: str | None, - ssl_verify_env: str | None, - ssl_verify_setting: bool, + ssl_verify: str | None, openssl_fips: bool, cryptography_fips: bool, refusal: str | None, ) -> None: - for name, value in (("LITELLM_FIPS_MODE", fips_mode), ("SSL_VERIFY", ssl_verify_env)): + for name, value in (("LITELLM_FIPS_MODE", fips_mode), ("SSL_VERIFY", ssl_verify)): if value is None: monkeypatch.delenv(name, raising=False) else: monkeypatch.setenv(name, value) - monkeypatch.setattr(litellm, "ssl_verify", ssl_verify_setting) _fake_openssl(monkeypatch, fips_flag=int(openssl_fips), md5_refused=openssl_fips) monkeypatch.setattr(rust_openssl, "is_fips_enabled", lambda: cryptography_fips) with pytest.raises(FipsModeError, match=refusal) if refusal else nullcontext(): enforce_fips_mode() - - -@pytest.mark.parametrize("fips_mode, refused", [("true", True), ("false", False)], ids=["fips", "fips-off"]) -def test_a_model_that_turns_tls_verification_off_is_refused_in_fips_mode( - monkeypatch: pytest.MonkeyPatch, fips_mode: str, refused: bool -) -> None: - monkeypatch.setenv("LITELLM_FIPS_MODE", fips_mode) - with pytest.raises(FipsModeError, match="or a model's ssl_verify") if refused else nullcontext(): - assert get_ssl_verify(ssl_verify=False) is False diff --git a/tests/unit/proxy/test_proxy_server_endpoints_and_startup.py b/tests/unit/proxy/test_proxy_server_endpoints_and_startup.py index 4bdadbba02e..ea238f74076 100644 --- a/tests/unit/proxy/test_proxy_server_endpoints_and_startup.py +++ b/tests/unit/proxy/test_proxy_server_endpoints_and_startup.py @@ -1775,24 +1775,6 @@ async def test_proxy_startup_refuses_an_unsafe_master_key_even_when_the_database assert ("could not be checked" in announced[0]) == key_can_have_encrypted_the_database -@pytest.mark.asyncio -async def test_proxy_startup_refuses_fips_mode_when_the_config_disables_tls_verification( - monkeypatch: pytest.MonkeyPatch, tmp_path: Path -) -> None: - from litellm.proxy.common_utils.fips import FipsModeError - from litellm.proxy.proxy_server import proxy_startup_event - - config_path, _ = _boot_with_general_settings(monkeypatch, tmp_path, {"master_key": "sk-a-safe-master-key"}) - config_path.write_text(yaml.dump({"litellm_settings": {"ssl_verify": False}})) - monkeypatch.setattr(litellm, "ssl_verify", True) - monkeypatch.delenv("SSL_VERIFY", raising=False) - monkeypatch.setenv("LITELLM_FIPS_MODE", "true") - - with pytest.raises(FipsModeError, match="TLS certificate verification is disabled"): - async with proxy_startup_event(FastAPI()): - pass - - class _UsersWithOnePlaintextPassword: async def find_many(self, where: object) -> tuple[types.SimpleNamespace, ...]: return (types.SimpleNamespace(user_id="user-1", password="a-plaintext-password"),) @@ -1826,8 +1808,6 @@ async def test_proxy_startup_refuses_fips_mode_when_the_password_migration_fails return real_new(name, data, usedforsecurity=usedforsecurity) _boot_with_general_settings(monkeypatch, tmp_path, {"master_key": "sk-a-safe-master-key"}) - monkeypatch.setattr(litellm, "ssl_verify", True) - monkeypatch.delenv("SSL_VERIFY", raising=False) monkeypatch.setenv("LITELLM_FIPS_MODE", "true") monkeypatch.setattr(_hashlib, "get_fips_mode", lambda: 1) monkeypatch.setattr(_hashlib, "new", new_refusing_md5)