From 4e18ab2f2f2a4c011b99b9a8e8efa9b4f55d5468 Mon Sep 17 00:00:00 2001 From: mateo Date: Fri, 11 Sep 2026 22:17:08 +0000 Subject: [PATCH 1/9] feat(proxy): warn at startup when the master key is the example sk-1234 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/proxy/auth/master_key_policy.py | 15 ++++++++++++ litellm/proxy/proxy_server.py | 5 ++++ .../proxy/auth/test_master_key_policy.py | 24 +++++++++++++++++++ .../proxy/proxy_server/test_lifecycle.py | 20 ++++++++++++++++ 4 files changed, 64 insertions(+) create mode 100644 litellm/proxy/auth/master_key_policy.py create mode 100644 tests/test_litellm/proxy/auth/test_master_key_policy.py diff --git a/litellm/proxy/auth/master_key_policy.py b/litellm/proxy/auth/master_key_policy.py new file mode 100644 index 00000000000..318b722b0ff --- /dev/null +++ b/litellm/proxy/auth/master_key_policy.py @@ -0,0 +1,15 @@ +from typing import Final + +INSECURE_MASTER_KEYS: Final = frozenset({"sk-1234"}) + + +def insecure_master_key_warning(master_key: str | None) -> str | None: + if master_key not in INSECURE_MASTER_KEYS: + return None + return ( + "LITELLM_MASTER_KEY is set to the example key 'sk-1234' from the docs. " + "Anyone who has read the docs can administer this gateway, and publicly reachable " + "gateways using this key have been compromised. Set a strong random master key " + "(e.g. `python -c \"import secrets; print('sk-' + secrets.token_urlsafe(32))\"`). " + "A future release will refuse to start with this key." + ) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index c8fd7edfed6..5aa75e360f2 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -324,6 +324,7 @@ from litellm.proxy.auth.auth_utils import ( from litellm.proxy.auth.fallback_model_access import router_fallback_access_check from litellm.proxy.auth.handle_jwt import JWTHandler from litellm.proxy.auth.litellm_license import AUTO_ROUTER_LICENSE_REMEDY, LicenseCheck +from litellm.proxy.auth.master_key_policy import insecure_master_key_warning from litellm.proxy.auth.model_checks import ( expand_wildcard_deployments_for_model_info, get_all_fallbacks, @@ -1159,6 +1160,10 @@ async def proxy_startup_event(app: FastAPI) -> AsyncGenerator[None, None]: if isinstance(worker_config, dict): await initialize(**worker_config) + _insecure_master_key_warning: Final = insecure_master_key_warning(master_key) + if _insecure_master_key_warning is not None: + verbose_proxy_logger.warning(_insecure_master_key_warning) + # check if DATABASE_URL in environment - load from there if prisma_client is None: _db_url: Final[str | None] = get_secret("DATABASE_URL", None) diff --git a/tests/test_litellm/proxy/auth/test_master_key_policy.py b/tests/test_litellm/proxy/auth/test_master_key_policy.py new file mode 100644 index 00000000000..6c949fc3783 --- /dev/null +++ b/tests/test_litellm/proxy/auth/test_master_key_policy.py @@ -0,0 +1,24 @@ +from litellm.litellm_core_utils.secret_redaction import redact_string +from litellm.proxy.auth.master_key_policy import insecure_master_key_warning + + +def test_insecure_master_key_warning_returned_for_example_key(): + warning = insecure_master_key_warning("sk-1234") + + assert warning is not None + assert "sk-1234" in warning + + +def test_insecure_master_key_warning_none_for_strong_key(): + assert insecure_master_key_warning("sk-strong-random-key") is None + + +def test_insecure_master_key_warning_none_for_none(): + assert insecure_master_key_warning(None) is None + + +def test_insecure_master_key_warning_survives_redaction(): + warning = insecure_master_key_warning("sk-1234") + + assert warning is not None + assert "secrets.token_urlsafe" in redact_string(warning) diff --git a/tests/test_litellm/proxy/proxy_server/test_lifecycle.py b/tests/test_litellm/proxy/proxy_server/test_lifecycle.py index deb7289d2d1..785a0dacdf2 100644 --- a/tests/test_litellm/proxy/proxy_server/test_lifecycle.py +++ b/tests/test_litellm/proxy/proxy_server/test_lifecycle.py @@ -1073,3 +1073,23 @@ async def test_prometheus_fallback_stats_job_runs_when_the_lock_is_free_or_absen await jobs["prometheus_fallback_stats_job"]() assert send_fallback_stats.await_count == 2 + + +@pytest.mark.asyncio +async def test_proxy_startup_event_warns_but_does_not_raise_for_docs_example_master_key(monkeypatch): + """With LITELLM_MASTER_KEY=sk-1234 the lifespan logs a loud warning and keeps booting.""" + monkeypatch.setenv("LITELLM_MASTER_KEY", "sk-1234") + + with patch.object(ps.verbose_proxy_logger, "warning") as mock_warning: + try: + async with proxy_startup_event(app=None): + pass + except ValueError as e: + if "sk-1234" in str(e): + pytest.fail("proxy_startup_event refused to boot on the docs example key") + except Exception: + pass + + assert any("sk-1234" in str(call.args[0]) for call in mock_warning.call_args_list), ( + "startup should log the insecure master key warning" + ) From 107b99c7337b612e33a02ab8aede66b3751921b1 Mon Sep 17 00:00:00 2001 From: mateo Date: Fri, 11 Sep 2026 22:17:56 +0000 Subject: [PATCH 2/9] test(proxy): drop docstring from startup warning test Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- tests/test_litellm/proxy/proxy_server/test_lifecycle.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_litellm/proxy/proxy_server/test_lifecycle.py b/tests/test_litellm/proxy/proxy_server/test_lifecycle.py index 785a0dacdf2..f592e9a8a7d 100644 --- a/tests/test_litellm/proxy/proxy_server/test_lifecycle.py +++ b/tests/test_litellm/proxy/proxy_server/test_lifecycle.py @@ -1077,7 +1077,6 @@ async def test_prometheus_fallback_stats_job_runs_when_the_lock_is_free_or_absen @pytest.mark.asyncio async def test_proxy_startup_event_warns_but_does_not_raise_for_docs_example_master_key(monkeypatch): - """With LITELLM_MASTER_KEY=sk-1234 the lifespan logs a loud warning and keeps booting.""" monkeypatch.setenv("LITELLM_MASTER_KEY", "sk-1234") with patch.object(ps.verbose_proxy_logger, "warning") as mock_warning: From 2f4032a984ff9b69d53fe36414d33e995c3c43a6 Mon Sep 17 00:00:00 2001 From: mateo Date: Fri, 11 Sep 2026 22:29:12 +0000 Subject: [PATCH 3/9] test: capture the startup warning with caplog instead of patching the logger Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../proxy/proxy_server/test_lifecycle.py | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/tests/test_litellm/proxy/proxy_server/test_lifecycle.py b/tests/test_litellm/proxy/proxy_server/test_lifecycle.py index f592e9a8a7d..775e9443653 100644 --- a/tests/test_litellm/proxy/proxy_server/test_lifecycle.py +++ b/tests/test_litellm/proxy/proxy_server/test_lifecycle.py @@ -1076,19 +1076,17 @@ async def test_prometheus_fallback_stats_job_runs_when_the_lock_is_free_or_absen @pytest.mark.asyncio -async def test_proxy_startup_event_warns_but_does_not_raise_for_docs_example_master_key(monkeypatch): +async def test_proxy_startup_event_warns_but_does_not_raise_for_docs_example_master_key(monkeypatch, caplog): monkeypatch.setenv("LITELLM_MASTER_KEY", "sk-1234") - with patch.object(ps.verbose_proxy_logger, "warning") as mock_warning: - try: + try: + with caplog.at_level(logging.WARNING, logger="LiteLLM Proxy"): async with proxy_startup_event(app=None): pass - except ValueError as e: - if "sk-1234" in str(e): - pytest.fail("proxy_startup_event refused to boot on the docs example key") - except Exception: - pass + except ValueError as e: + if "sk-1234" in str(e): + pytest.fail("proxy_startup_event refused to boot on the docs example key") + except Exception: + pass - assert any("sk-1234" in str(call.args[0]) for call in mock_warning.call_args_list), ( - "startup should log the insecure master key warning" - ) + assert "sk-1234" in caplog.text, "startup should log the insecure master key warning" From d729121a142ea9054004cb33024ff3f1cfbada27 Mon Sep 17 00:00:00 2001 From: mateo Date: Fri, 11 Sep 2026 22:47:21 +0000 Subject: [PATCH 4/9] feat(proxy): warn at startup when no master key is configured --- litellm/proxy/auth/master_key_policy.py | 27 +++++++++++------- litellm/proxy/proxy_server.py | 7 ++++- .../proxy/auth/test_master_key_policy.py | 28 ++++++++++++++----- .../proxy/proxy_server/test_lifecycle.py | 17 +++++++++++ 4 files changed, 61 insertions(+), 18 deletions(-) diff --git a/litellm/proxy/auth/master_key_policy.py b/litellm/proxy/auth/master_key_policy.py index 318b722b0ff..eeb7d3ee72a 100644 --- a/litellm/proxy/auth/master_key_policy.py +++ b/litellm/proxy/auth/master_key_policy.py @@ -3,13 +3,20 @@ from typing import Final INSECURE_MASTER_KEYS: Final = frozenset({"sk-1234"}) -def insecure_master_key_warning(master_key: str | None) -> str | None: - if master_key not in INSECURE_MASTER_KEYS: - return None - return ( - "LITELLM_MASTER_KEY is set to the example key 'sk-1234' from the docs. " - "Anyone who has read the docs can administer this gateway, and publicly reachable " - "gateways using this key have been compromised. Set a strong random master key " - "(e.g. `python -c \"import secrets; print('sk-' + secrets.token_urlsafe(32))\"`). " - "A future release will refuse to start with this key." - ) +def insecure_master_key_warning(master_key: str | None, alternative_auth_enabled: bool) -> str | None: + if master_key in INSECURE_MASTER_KEYS: + return ( + "LITELLM_MASTER_KEY is set to the example key 'sk-1234' from the docs. " + "Anyone who has read the docs can administer this gateway, and publicly reachable " + "gateways using this key have been compromised. Set a strong random master key " + "(e.g. `python -c \"import secrets; print('sk-' + secrets.token_urlsafe(32))\"`). " + "A future release will refuse to start with this key." + ) + if (master_key is None or master_key == "") and not alternative_auth_enabled: + return ( + "No master key is set (LITELLM_MASTER_KEY or general_settings.master_key). " + "Every request to this proxy is accepted without authentication, including " + 'admin routes. Set a strong random master key (e.g. `python -c "import secrets; ' + "print('sk-' + secrets.token_urlsafe(32))\"`) before exposing it to a network." + ) + return None diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 5aa75e360f2..805c61d4697 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -1160,7 +1160,12 @@ async def proxy_startup_event(app: FastAPI) -> AsyncGenerator[None, None]: if isinstance(worker_config, dict): await initialize(**worker_config) - _insecure_master_key_warning: Final = insecure_master_key_warning(master_key) + _alternative_auth_enabled: Final = any( + general_settings.get(k, False) for k in ("enable_jwt_auth", "enable_oauth2_auth", "enable_oauth2_proxy_auth") + ) + _insecure_master_key_warning: Final = insecure_master_key_warning( + master_key, alternative_auth_enabled=_alternative_auth_enabled + ) if _insecure_master_key_warning is not None: verbose_proxy_logger.warning(_insecure_master_key_warning) diff --git a/tests/test_litellm/proxy/auth/test_master_key_policy.py b/tests/test_litellm/proxy/auth/test_master_key_policy.py index 6c949fc3783..6b118994cba 100644 --- a/tests/test_litellm/proxy/auth/test_master_key_policy.py +++ b/tests/test_litellm/proxy/auth/test_master_key_policy.py @@ -3,22 +3,36 @@ from litellm.proxy.auth.master_key_policy import insecure_master_key_warning def test_insecure_master_key_warning_returned_for_example_key(): - warning = insecure_master_key_warning("sk-1234") + warning = insecure_master_key_warning("sk-1234", alternative_auth_enabled=False) assert warning is not None assert "sk-1234" in warning +def test_insecure_master_key_warning_for_missing_key(): + warning = insecure_master_key_warning(None, alternative_auth_enabled=False) + + assert warning is not None + assert "No master key" in warning + + +def test_insecure_master_key_warning_for_empty_key(): + warning = insecure_master_key_warning("", alternative_auth_enabled=False) + + assert warning is not None + assert "No master key" in warning + + +def test_insecure_master_key_warning_none_for_missing_key_with_alt_auth(): + assert insecure_master_key_warning(None, alternative_auth_enabled=True) is None + + def test_insecure_master_key_warning_none_for_strong_key(): - assert insecure_master_key_warning("sk-strong-random-key") is None - - -def test_insecure_master_key_warning_none_for_none(): - assert insecure_master_key_warning(None) is None + assert insecure_master_key_warning("sk-strong-random-key", alternative_auth_enabled=False) is None def test_insecure_master_key_warning_survives_redaction(): - warning = insecure_master_key_warning("sk-1234") + warning = insecure_master_key_warning("sk-1234", alternative_auth_enabled=False) assert warning is not None assert "secrets.token_urlsafe" in redact_string(warning) diff --git a/tests/test_litellm/proxy/proxy_server/test_lifecycle.py b/tests/test_litellm/proxy/proxy_server/test_lifecycle.py index 775e9443653..d472e3935de 100644 --- a/tests/test_litellm/proxy/proxy_server/test_lifecycle.py +++ b/tests/test_litellm/proxy/proxy_server/test_lifecycle.py @@ -1090,3 +1090,20 @@ async def test_proxy_startup_event_warns_but_does_not_raise_for_docs_example_mas pass assert "sk-1234" in caplog.text, "startup should log the insecure master key warning" + + +@pytest.mark.asyncio +async def test_proxy_startup_event_warns_but_does_not_raise_for_missing_master_key(monkeypatch, caplog): + monkeypatch.delenv("LITELLM_MASTER_KEY", raising=False) + + try: + with caplog.at_level(logging.WARNING, logger="LiteLLM Proxy"): + async with proxy_startup_event(app=None): + pass + except ValueError as e: + if "master key" in str(e).lower(): + pytest.fail("proxy_startup_event refused to boot without a master key") + except Exception: + pass + + assert "No master key" in caplog.text, "startup should log the missing master key warning" From ebc60204357c02951a2cf4c6540a48445a58261d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 11 Sep 2026 23:21:00 +0000 Subject: [PATCH 5/9] feat(ui): warn admins when the master key is sk-1234 or missing --- litellm/proxy/auth/master_key_policy.py | 58 +++++++++++++------ .../health_endpoints/_health_endpoints.py | 14 +++++ litellm/proxy/proxy_server.py | 7 +-- .../proxy/auth/test_master_key_policy.py | 19 +++++- .../health_endpoints/test_health_endpoints.py | 33 +++++++++++ .../useHealthReadinessDetails.ts | 1 + .../src/app/(dashboard)/layout.tsx | 3 + .../InsecureMasterKeyWarningBanner.test.tsx | 50 ++++++++++++++++ .../InsecureMasterKeyWarningBanner.tsx | 52 +++++++++++++++++ 9 files changed, 214 insertions(+), 23 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/InsecureMasterKeyWarningBanner.test.tsx create mode 100644 ui/litellm-dashboard/src/components/InsecureMasterKeyWarningBanner.tsx diff --git a/litellm/proxy/auth/master_key_policy.py b/litellm/proxy/auth/master_key_policy.py index eeb7d3ee72a..f8fcd7d9b8e 100644 --- a/litellm/proxy/auth/master_key_policy.py +++ b/litellm/proxy/auth/master_key_policy.py @@ -1,22 +1,46 @@ -from typing import Final +from typing import Final, Literal + +from typing_extensions import assert_never INSECURE_MASTER_KEYS: Final = frozenset({"sk-1234"}) +InsecureMasterKeyReason = Literal["example_key", "missing"] + +_ALTERNATIVE_AUTH_SETTINGS: Final = ("enable_jwt_auth", "enable_oauth2_auth", "enable_oauth2_proxy_auth") + + +def alternative_auth_enabled(general_settings: dict) -> bool: + return any(general_settings.get(k, False) for k in _ALTERNATIVE_AUTH_SETTINGS) + + +def insecure_master_key_reason( + master_key: str | None, alternative_auth_enabled: bool +) -> InsecureMasterKeyReason | None: + if master_key in INSECURE_MASTER_KEYS: + return "example_key" + if (master_key is None or master_key == "") and not alternative_auth_enabled: + return "missing" + return None + def insecure_master_key_warning(master_key: str | None, alternative_auth_enabled: bool) -> str | None: - if master_key in INSECURE_MASTER_KEYS: - return ( - "LITELLM_MASTER_KEY is set to the example key 'sk-1234' from the docs. " - "Anyone who has read the docs can administer this gateway, and publicly reachable " - "gateways using this key have been compromised. Set a strong random master key " - "(e.g. `python -c \"import secrets; print('sk-' + secrets.token_urlsafe(32))\"`). " - "A future release will refuse to start with this key." - ) - if (master_key is None or master_key == "") and not alternative_auth_enabled: - return ( - "No master key is set (LITELLM_MASTER_KEY or general_settings.master_key). " - "Every request to this proxy is accepted without authentication, including " - 'admin routes. Set a strong random master key (e.g. `python -c "import secrets; ' - "print('sk-' + secrets.token_urlsafe(32))\"`) before exposing it to a network." - ) - return None + match insecure_master_key_reason(master_key, alternative_auth_enabled): + case "example_key": + return ( + "LITELLM_MASTER_KEY is set to the example key 'sk-1234' from the docs. " + "Anyone who has read the docs can administer this gateway, and publicly reachable " + "gateways using this key have been compromised. Set a strong random master key " + "(e.g. `python -c \"import secrets; print('sk-' + secrets.token_urlsafe(32))\"`). " + "A future release will refuse to start with this key." + ) + case "missing": + return ( + "No master key is set (LITELLM_MASTER_KEY or general_settings.master_key). " + "Every request to this proxy is accepted without authentication, including " + 'admin routes. Set a strong random master key (e.g. `python -c "import secrets; ' + "print('sk-' + secrets.token_urlsafe(32))\"`) before exposing it to a network." + ) + case None: + return None + case _ as unreachable: + assert_never(unreachable) diff --git a/litellm/proxy/health_endpoints/_health_endpoints.py b/litellm/proxy/health_endpoints/_health_endpoints.py index bf527e1e868..475780cb005 100644 --- a/litellm/proxy/health_endpoints/_health_endpoints.py +++ b/litellm/proxy/health_endpoints/_health_endpoints.py @@ -39,6 +39,11 @@ from litellm.proxy._types import ( from litellm.proxy.auth.auth_utils import ( _BANNED_REQUEST_BODY_PARAMS, # pyright: ignore[reportPrivateUsage] # one canonical list, shared with the request-body check ) +from litellm.proxy.auth.master_key_policy import ( + InsecureMasterKeyReason, + alternative_auth_enabled, + insecure_master_key_reason, +) from litellm.proxy.auth.user_api_key_auth import user_api_key_auth from litellm.proxy.db.exception_handler import PrismaDBExceptionHandler from litellm.proxy.db.health_check_latest import LatestHealthCheckRow @@ -1639,6 +1644,12 @@ def _show_env_credential_login_warning() -> bool: return is_env_credential_login_enabled(general_settings) +def _insecure_master_key_reason() -> InsecureMasterKeyReason | None: + from litellm.proxy.proxy_server import general_settings, master_key + + return insecure_master_key_reason(master_key, alternative_auth_enabled=alternative_auth_enabled(general_settings)) + + async def _get_health_readiness_details( response: Response | None = None, ) -> dict[str, Any]: @@ -1681,6 +1692,7 @@ async def _get_health_readiness_details( is_detailed_debug: Final = verbose_logger.isEnabledFor(logging.DEBUG) show_no_redis_warning: Final = await _show_no_redis_warning() show_env_credential_login_warning: Final = _show_env_credential_login_warning() + insecure_master_key_reason: Final = _insecure_master_key_reason() # check DB if prisma_client is not None: # if db passed in, check if it's connected @@ -1709,6 +1721,7 @@ async def _get_health_readiness_details( "is_detailed_debug": is_detailed_debug, "show_no_redis_warning": show_no_redis_warning, "show_env_credential_login_warning": show_env_credential_login_warning, + "insecure_master_key_reason": insecure_master_key_reason, } else: return { @@ -1722,6 +1735,7 @@ async def _get_health_readiness_details( "is_detailed_debug": is_detailed_debug, "show_no_redis_warning": show_no_redis_warning, "show_env_credential_login_warning": show_env_credential_login_warning, + "insecure_master_key_reason": insecure_master_key_reason, } except Exception as e: raise HTTPException(status_code=503, detail=f"Service Unhealthy ({e})") diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 805c61d4697..b16e09b8948 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -324,7 +324,7 @@ from litellm.proxy.auth.auth_utils import ( from litellm.proxy.auth.fallback_model_access import router_fallback_access_check from litellm.proxy.auth.handle_jwt import JWTHandler from litellm.proxy.auth.litellm_license import AUTO_ROUTER_LICENSE_REMEDY, LicenseCheck -from litellm.proxy.auth.master_key_policy import insecure_master_key_warning +from litellm.proxy.auth.master_key_policy import alternative_auth_enabled, insecure_master_key_warning from litellm.proxy.auth.model_checks import ( expand_wildcard_deployments_for_model_info, get_all_fallbacks, @@ -1160,11 +1160,8 @@ async def proxy_startup_event(app: FastAPI) -> AsyncGenerator[None, None]: if isinstance(worker_config, dict): await initialize(**worker_config) - _alternative_auth_enabled: Final = any( - general_settings.get(k, False) for k in ("enable_jwt_auth", "enable_oauth2_auth", "enable_oauth2_proxy_auth") - ) _insecure_master_key_warning: Final = insecure_master_key_warning( - master_key, alternative_auth_enabled=_alternative_auth_enabled + master_key, alternative_auth_enabled=alternative_auth_enabled(general_settings) ) if _insecure_master_key_warning is not None: verbose_proxy_logger.warning(_insecure_master_key_warning) diff --git a/tests/test_litellm/proxy/auth/test_master_key_policy.py b/tests/test_litellm/proxy/auth/test_master_key_policy.py index 6b118994cba..3f069e380c4 100644 --- a/tests/test_litellm/proxy/auth/test_master_key_policy.py +++ b/tests/test_litellm/proxy/auth/test_master_key_policy.py @@ -1,5 +1,22 @@ from litellm.litellm_core_utils.secret_redaction import redact_string -from litellm.proxy.auth.master_key_policy import insecure_master_key_warning +from litellm.proxy.auth.master_key_policy import insecure_master_key_reason, insecure_master_key_warning + + +def test_insecure_master_key_reason_for_example_key(): + assert insecure_master_key_reason("sk-1234", alternative_auth_enabled=False) == "example_key" + + +def test_insecure_master_key_reason_for_missing_key(): + assert insecure_master_key_reason(None, alternative_auth_enabled=False) == "missing" + assert insecure_master_key_reason("", alternative_auth_enabled=False) == "missing" + + +def test_insecure_master_key_reason_none_for_strong_key(): + assert insecure_master_key_reason("sk-strong-random-key", alternative_auth_enabled=False) is None + + +def test_insecure_master_key_reason_none_with_alternative_auth(): + assert insecure_master_key_reason(None, alternative_auth_enabled=True) is None def test_insecure_master_key_warning_returned_for_example_key(): diff --git a/tests/test_litellm/proxy/health_endpoints/test_health_endpoints.py b/tests/test_litellm/proxy/health_endpoints/test_health_endpoints.py index 527d46931fe..c8e47c0f02a 100644 --- a/tests/test_litellm/proxy/health_endpoints/test_health_endpoints.py +++ b/tests/test_litellm/proxy/health_endpoints/test_health_endpoints.py @@ -1357,6 +1357,39 @@ def test_health_readiness_details_reports_env_credential_login_warning(monkeypat assert response.json()["show_env_credential_login_warning"] is expected_warning +@pytest.mark.parametrize( + "master_key, general_settings, expected_reason", + [ + ("sk-1234", {}, "example_key"), + (None, {}, "missing"), + ("", {}, "missing"), + (None, {"enable_jwt_auth": True}, None), + ("sk-strong-random-key", {}, None), + ], +) +def test_health_readiness_details_reports_insecure_master_key_reason( + monkeypatch, master_key, general_settings, expected_reason +): + """ + The Admin UI banner is driven by this field: it must be "example_key" while + the docs example key is configured, "missing" when no master key is set and + no alternative auth replaces it, and null when the configured key is strong. + """ + app = FastAPI() + app.include_router(_health_endpoints_module.router) + app.dependency_overrides[user_api_key_auth] = lambda: UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN) + client = TestClient(app) + + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", None) + monkeypatch.setattr("litellm.proxy.proxy_server.master_key", master_key) + monkeypatch.setattr("litellm.proxy.proxy_server.general_settings", general_settings) + + response = client.get("/health/readiness/details") + + assert response.status_code == 200, response.text + assert response.json()["insecure_master_key_reason"] == expected_reason + + def test_health_readiness_allows_explicit_legacy_public_details(monkeypatch): """ Operators can explicitly preserve the legacy public readiness payload. diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/healthReadiness/useHealthReadinessDetails.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/healthReadiness/useHealthReadinessDetails.ts index 44d9092df34..e76c894fd8f 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/healthReadiness/useHealthReadinessDetails.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/healthReadiness/useHealthReadinessDetails.ts @@ -15,6 +15,7 @@ export interface HealthReadinessDetailsResponse { is_detailed_debug?: boolean; show_no_redis_warning?: boolean; show_env_credential_login_warning?: boolean; + insecure_master_key_reason?: "example_key" | "missing" | null; } const fetchHealthReadinessDetails = async (accessToken: string): Promise => { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx b/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx index fa6df7f176a..0152ec90d28 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx @@ -11,6 +11,7 @@ import { useRouter, useSearchParams } from "next/navigation"; import { DebugWarningBanner } from "@/components/DebugWarningBanner"; import { NoRedisWarningBanner } from "@/components/NoRedisWarningBanner"; import { EnvCredentialLoginWarningBanner } from "@/components/EnvCredentialLoginWarningBanner"; +import { InsecureMasterKeyWarningBanner } from "@/components/InsecureMasterKeyWarningBanner"; import { LicenseExpiryBanner } from "@/components/LicenseExpiryBanner"; import { UserBanner } from "@/components/UserBanner"; import { uiHref } from "@/utils/uiHref"; @@ -115,6 +116,7 @@ function DashboardShell({ children }: { children: React.ReactNode }) { +
@@ -135,6 +137,7 @@ function DashboardShell({ children }: { children: React.ReactNode }) { +
{children}
diff --git a/ui/litellm-dashboard/src/components/InsecureMasterKeyWarningBanner.test.tsx b/ui/litellm-dashboard/src/components/InsecureMasterKeyWarningBanner.test.tsx new file mode 100644 index 00000000000..3c6ad94c8ed --- /dev/null +++ b/ui/litellm-dashboard/src/components/InsecureMasterKeyWarningBanner.test.tsx @@ -0,0 +1,50 @@ +import { renderWithProviders, screen } from "../../tests/test-utils"; +import { vi } from "vitest"; +import { InsecureMasterKeyWarningBanner } from "./InsecureMasterKeyWarningBanner"; +import type { HealthReadinessDetailsResponse } from "@/app/(dashboard)/hooks/healthReadiness/useHealthReadinessDetails"; +import type { UseQueryResult } from "@tanstack/react-query"; + +vi.mock("@/app/(dashboard)/hooks/healthReadiness/useHealthReadinessDetails", () => ({ + useHealthReadinessDetails: vi.fn(), +})); + +import { useHealthReadinessDetails } from "@/app/(dashboard)/hooks/healthReadiness/useHealthReadinessDetails"; + +const mockDetails = (data: Partial | undefined) => { + vi.mocked(useHealthReadinessDetails).mockReturnValue({ data } as UseQueryResult); +}; + +describe("InsecureMasterKeyWarningBanner", () => { + it("should warn when the proxy reports the docs example key", () => { + mockDetails({ status: "healthy", insecure_master_key_reason: "example_key" }); + renderWithProviders(); + expect(screen.getByRole("alert")).toBeInTheDocument(); + expect(screen.getByText("The master key is the docs example key sk-1234")).toBeInTheDocument(); + }); + + it("should warn when the proxy reports no master key", () => { + mockDetails({ status: "healthy", insecure_master_key_reason: "missing" }); + renderWithProviders(); + expect(screen.getByRole("alert")).toBeInTheDocument(); + expect(screen.getByText("No master key is set")).toBeInTheDocument(); + expect(screen.getByText(/accepted without authentication/)).toBeInTheDocument(); + }); + + it("should render nothing when the configured key is strong", () => { + mockDetails({ status: "healthy", insecure_master_key_reason: null }); + const { container } = renderWithProviders(); + expect(container).toBeEmptyDOMElement(); + }); + + it("should render nothing when readiness details are unavailable", () => { + mockDetails(undefined); + const { container } = renderWithProviders(); + expect(container).toBeEmptyDOMElement(); + }); + + it("should pass the access token to the readiness hook", () => { + mockDetails(undefined); + renderWithProviders(); + expect(useHealthReadinessDetails).toHaveBeenCalledWith("my-token"); + }); +}); diff --git a/ui/litellm-dashboard/src/components/InsecureMasterKeyWarningBanner.tsx b/ui/litellm-dashboard/src/components/InsecureMasterKeyWarningBanner.tsx new file mode 100644 index 00000000000..27d6f927d47 --- /dev/null +++ b/ui/litellm-dashboard/src/components/InsecureMasterKeyWarningBanner.tsx @@ -0,0 +1,52 @@ +"use client"; + +import React from "react"; +import { TriangleAlert } from "lucide-react"; +import { useHealthReadinessDetails } from "@/app/(dashboard)/hooks/healthReadiness/useHealthReadinessDetails"; + +const BANNER_CONTENT = { + example_key: { + title: "The master key is the docs example key sk-1234", + body: ( + <> + Anyone who has read the LiteLLM docs can administer this gateway. Generate a strong random key, set it as{" "} + LITELLM_MASTER_KEY (or{" "} + general_settings.master_key), and restart the proxy. + + ), + }, + missing: { + title: "No master key is set", + body: ( + <> + Every request to this proxy is accepted without authentication, including admin routes. Set{" "} + LITELLM_MASTER_KEY (or{" "} + general_settings.master_key) to a strong random key and restart the proxy. + + ), + }, +} as const; + +export const InsecureMasterKeyWarningBanner: React.FC<{ accessToken: string | null }> = ({ accessToken }) => { + const { data: healthData } = useHealthReadinessDetails(accessToken); + const reason = healthData?.insecure_master_key_reason; + + if (reason == null) { + return null; + } + + const { title, body } = BANNER_CONTENT[reason]; + + return ( +
+
+ ); +}; From 17dc6d24e96e7a4777302a02c9cccf2890db627f Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 11 Sep 2026 23:24:48 +0000 Subject: [PATCH 6/9] test(ui): stub InsecureMasterKeyWarningBanner in the layout test --- ui/litellm-dashboard/src/app/(dashboard)/layout.test.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/layout.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/layout.test.tsx index 3fe34610260..7cba39ac50f 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/layout.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/layout.test.tsx @@ -33,6 +33,10 @@ vi.mock("@/components/EnvCredentialLoginWarningBanner", () => ({ EnvCredentialLoginWarningBanner: () => null, })); +vi.mock("@/components/InsecureMasterKeyWarningBanner", () => ({ + InsecureMasterKeyWarningBanner: () => null, +})); + vi.mock("@/components/LicenseExpiryBanner", () => ({ LicenseExpiryBanner: () => null, })); From 741614b70a64eda4640486db58deb7363f85759b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 11 Sep 2026 23:31:48 +0000 Subject: [PATCH 7/9] refactor: move the exhaustiveness check off the Never-narrowed match arm --- litellm/proxy/auth/master_key_policy.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/litellm/proxy/auth/master_key_policy.py b/litellm/proxy/auth/master_key_policy.py index f8fcd7d9b8e..23f39f1cf03 100644 --- a/litellm/proxy/auth/master_key_policy.py +++ b/litellm/proxy/auth/master_key_policy.py @@ -1,3 +1,4 @@ +from collections.abc import Mapping from typing import Final, Literal from typing_extensions import assert_never @@ -9,7 +10,7 @@ InsecureMasterKeyReason = Literal["example_key", "missing"] _ALTERNATIVE_AUTH_SETTINGS: Final = ("enable_jwt_auth", "enable_oauth2_auth", "enable_oauth2_proxy_auth") -def alternative_auth_enabled(general_settings: dict) -> bool: +def alternative_auth_enabled(general_settings: Mapping[str, object]) -> bool: return any(general_settings.get(k, False) for k in _ALTERNATIVE_AUTH_SETTINGS) @@ -24,7 +25,8 @@ def insecure_master_key_reason( def insecure_master_key_warning(master_key: str | None, alternative_auth_enabled: bool) -> str | None: - match insecure_master_key_reason(master_key, alternative_auth_enabled): + reason: Final = insecure_master_key_reason(master_key, alternative_auth_enabled) + match reason: case "example_key": return ( "LITELLM_MASTER_KEY is set to the example key 'sk-1234' from the docs. " @@ -42,5 +44,4 @@ def insecure_master_key_warning(master_key: str | None, alternative_auth_enabled ) case None: return None - case _ as unreachable: - assert_never(unreachable) + assert_never(reason) From 42e221fe9006f8d6621a60d315449c223cd8600a Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 11 Sep 2026 23:43:50 +0000 Subject: [PATCH 8/9] fix(proxy): treat custom_auth as alternative auth for the missing master key warning --- litellm/proxy/auth/master_key_policy.py | 2 +- .../proxy/auth/test_master_key_policy.py | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/auth/master_key_policy.py b/litellm/proxy/auth/master_key_policy.py index 23f39f1cf03..3b34be0bb07 100644 --- a/litellm/proxy/auth/master_key_policy.py +++ b/litellm/proxy/auth/master_key_policy.py @@ -7,7 +7,7 @@ INSECURE_MASTER_KEYS: Final = frozenset({"sk-1234"}) InsecureMasterKeyReason = Literal["example_key", "missing"] -_ALTERNATIVE_AUTH_SETTINGS: Final = ("enable_jwt_auth", "enable_oauth2_auth", "enable_oauth2_proxy_auth") +_ALTERNATIVE_AUTH_SETTINGS: Final = ("enable_jwt_auth", "enable_oauth2_auth", "enable_oauth2_proxy_auth", "custom_auth") def alternative_auth_enabled(general_settings: Mapping[str, object]) -> bool: diff --git a/tests/test_litellm/proxy/auth/test_master_key_policy.py b/tests/test_litellm/proxy/auth/test_master_key_policy.py index 3f069e380c4..1b7d5a086f8 100644 --- a/tests/test_litellm/proxy/auth/test_master_key_policy.py +++ b/tests/test_litellm/proxy/auth/test_master_key_policy.py @@ -1,5 +1,11 @@ +from typing import Final + from litellm.litellm_core_utils.secret_redaction import redact_string -from litellm.proxy.auth.master_key_policy import insecure_master_key_reason, insecure_master_key_warning +from litellm.proxy.auth.master_key_policy import ( + alternative_auth_enabled, + insecure_master_key_reason, + insecure_master_key_warning, +) def test_insecure_master_key_reason_for_example_key(): @@ -19,6 +25,12 @@ def test_insecure_master_key_reason_none_with_alternative_auth(): assert insecure_master_key_reason(None, alternative_auth_enabled=True) is None +def test_insecure_master_key_reason_none_with_custom_auth(): + general_settings: Final = {"custom_auth": "my_package.custom_auth_handler"} + + assert insecure_master_key_reason(None, alternative_auth_enabled=alternative_auth_enabled(general_settings)) is None + + def test_insecure_master_key_warning_returned_for_example_key(): warning = insecure_master_key_warning("sk-1234", alternative_auth_enabled=False) From bac482df581d2495d609a95b836f500ba2305c52 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 12 Sep 2026 04:37:16 +0000 Subject: [PATCH 9/9] test: drop the restating docstring on the readiness reason test --- .../proxy/health_endpoints/test_health_endpoints.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/test_litellm/proxy/health_endpoints/test_health_endpoints.py b/tests/test_litellm/proxy/health_endpoints/test_health_endpoints.py index 40c69a2989b..1bfc3ad4fcb 100644 --- a/tests/test_litellm/proxy/health_endpoints/test_health_endpoints.py +++ b/tests/test_litellm/proxy/health_endpoints/test_health_endpoints.py @@ -1374,11 +1374,6 @@ def test_health_readiness_details_reports_env_credential_login_warning(monkeypat def test_health_readiness_details_reports_insecure_master_key_reason( monkeypatch, master_key, general_settings, expected_reason ): - """ - The Admin UI banner is driven by this field: it must be "example_key" while - the docs example key is configured, "missing" when no master key is set and - no alternative auth replaces it, and null when the configured key is strong. - """ app = FastAPI() app.include_router(_health_endpoints_module.router) app.dependency_overrides[user_api_key_auth] = lambda: UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN)