mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-02 07:13:02 +00:00
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:
parent
6103a8c60c
commit
bd24880ee5
5 changed files with 16 additions and 97 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 "
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue