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.
This commit is contained in:
yucheng 2026-10-30 18:03:09 -07:00
parent 6103a8c60c
commit bd24880ee5
5 changed files with 16 additions and 97 deletions

View file

@ -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.

View file

@ -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 "

View file

@ -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

View file

@ -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

View file

@ -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)