From 74c0e1a339815bf78421c7367e3b15b4ad4e116f Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 13:52:30 +0000 Subject: [PATCH] feat(proxy): lock out credential storage and virtual key management while the master key is insecure --- litellm/proxy/auth/master_key_policy.py | 110 +++++++++++++++++- litellm/proxy/auth/route_checks.py | 4 + litellm/proxy/auth/user_api_key_auth.py | 19 +++ .../health_endpoints/_health_endpoints.py | 4 + .../proxy/auth/test_master_key_policy.py | 80 +++++++++++++ .../proxy/auth/test_user_api_key_auth.py | 106 +++++++++++++++++ .../health_endpoints/test_health_endpoints.py | 26 +++-- .../useHealthReadinessDetails.ts | 1 + .../InsecureMasterKeyWarningBanner.test.tsx | 20 +++- .../InsecureMasterKeyWarningBanner.tsx | 45 ++++--- 10 files changed, 375 insertions(+), 40 deletions(-) diff --git a/litellm/proxy/auth/master_key_policy.py b/litellm/proxy/auth/master_key_policy.py index 3b34be0bb07..1fec63b43fd 100644 --- a/litellm/proxy/auth/master_key_policy.py +++ b/litellm/proxy/auth/master_key_policy.py @@ -1,7 +1,11 @@ -from collections.abc import Mapping -from typing import Final, Literal +from collections.abc import Mapping, Sequence +from typing import Final, Literal, cast -from typing_extensions import assert_never +from pydantic import TypeAdapter +from typing_extensions import TypedDict, assert_never + +from litellm.proxy._types import LiteLLMRoutes, ProxyErrorTypes, ProxyException +from litellm.proxy.auth.route_checks import RouteChecks INSECURE_MASTER_KEYS: Final = frozenset({"sk-1234"}) @@ -9,6 +13,51 @@ InsecureMasterKeyReason = Literal["example_key", "missing"] _ALTERNATIVE_AUTH_SETTINGS: Final = ("enable_jwt_auth", "enable_oauth2_auth", "enable_oauth2_proxy_auth", "custom_auth") +LockoutAction = Literal["store_credentials", "access_credentials", "use_credentials", "manage_virtual_keys"] + +MASTER_KEY_LOCKOUT_MESSAGE: Final = ( + "This functionality is unavailable until the master key has been set. " + "Set LITELLM_MASTER_KEY (or general_settings.master_key) to a strong random key and restart the proxy." +) + +_STORE_CREDENTIAL_ROUTES: Final = ( + "/credentials", + "/credentials/{credential_name:path}", + "/model/new", + "/model/update", + "/model/{model_id}/update", + "/config/update", +) + + +class _ModelInfoMarker(TypedDict, total=False): + db_model: bool + + +class _DeploymentMarker(TypedDict, total=False): + model_info: _ModelInfoMarker + + +_DEPLOYMENT_MARKERS: Final = TypeAdapter(list[_DeploymentMarker]) + +_ACCESS_CREDENTIAL_ROUTES: Final = ( + "/credentials", + "/credentials/by_name/{credential_name:path}", + "/credentials/by_model", + "/model/info", + "/v1/model/info", + "/v2/model/info", + "/get/config/callbacks", + "/config/list", + "/config/field/info", +) + + +def _route_matches_any(route: str, patterns: Sequence[str]) -> bool: + return any( + route == pattern or RouteChecks.route_matches_pattern(route=route, pattern=pattern) for pattern in patterns + ) + def alternative_auth_enabled(general_settings: Mapping[str, object]) -> bool: return any(general_settings.get(k, False) for k in _ALTERNATIVE_AUTH_SETTINGS) @@ -33,15 +82,66 @@ def insecure_master_key_warning(master_key: str | None, alternative_auth_enabled "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." + "Storing and using upstream credentials and managing virtual keys are disabled " + "until a strong master key is set." ) 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." + "print('sk-' + secrets.token_urlsafe(32))\"`) before exposing it to a network. " + "Storing and using upstream credentials and managing virtual keys are disabled " + "until a strong master key is set." ) case None: return None assert_never(reason) + + +def stored_credentials_present() -> bool: + import litellm + + if litellm.credential_list: + return True + from litellm import Router + from litellm.proxy.proxy_server import llm_router + + if not isinstance(llm_router, Router): + return False + model_list: Final = cast( + "list[object]", + llm_router.model_list or [], # pyright: ignore[reportUnknownMemberType] # Router.model_list is declared bare `list`; elements are validated by the TypeAdapter below + ) + deployments: Final = _DEPLOYMENT_MARKERS.validate_python(model_list) + return any((d.get("model_info") or {}).get("db_model") is True for d in deployments) + + +def master_key_lockout_action( + route: str, + method: str, + reason: InsecureMasterKeyReason | None, + stored_credentials_present: bool, +) -> LockoutAction | None: + if reason is None: + return None + if method.upper() != "GET" and _route_matches_any(route, _STORE_CREDENTIAL_ROUTES): + return "store_credentials" + if not stored_credentials_present: + return None + if method.upper() == "GET" and _route_matches_any(route, _ACCESS_CREDENTIAL_ROUTES): + return "access_credentials" + if method.upper() != "GET" and RouteChecks.is_llm_api_route(route=route): + return "use_credentials" + if method.upper() != "GET" and _route_matches_any(route, tuple(LiteLLMRoutes.key_management_routes.value)): + return "manage_virtual_keys" + return None + + +def master_key_lockout_exception(action: LockoutAction) -> ProxyException: + return ProxyException( + message=MASTER_KEY_LOCKOUT_MESSAGE, + type=ProxyErrorTypes.auth_error, + param="master_key", + code=403, + ) diff --git a/litellm/proxy/auth/route_checks.py b/litellm/proxy/auth/route_checks.py index c4109bdc2f1..58952bebb85 100644 --- a/litellm/proxy/auth/route_checks.py +++ b/litellm/proxy/auth/route_checks.py @@ -486,6 +486,10 @@ class RouteChecks: return True return False + @staticmethod + def route_matches_pattern(route: str, pattern: str) -> bool: + return RouteChecks._route_matches_pattern(route=route, pattern=pattern) + @staticmethod def _route_matches_pattern(route: str, pattern: str) -> bool: """ diff --git a/litellm/proxy/auth/user_api_key_auth.py b/litellm/proxy/auth/user_api_key_auth.py index 687f36bbe8b..f49c7272921 100644 --- a/litellm/proxy/auth/user_api_key_auth.py +++ b/litellm/proxy/auth/user_api_key_auth.py @@ -79,6 +79,13 @@ from litellm.proxy.auth.auth_utils import ( route_in_additonal_public_routes, ) from litellm.proxy.auth.handle_jwt import JWTAuthManager, JWTHandler +from litellm.proxy.auth.master_key_policy import ( + alternative_auth_enabled, + insecure_master_key_reason, + master_key_lockout_action, + master_key_lockout_exception, + stored_credentials_present, +) from litellm.proxy.auth.network import TrustedProxyConfig, resolve_network_context from litellm.proxy.auth.oauth2_check import Oauth2Handler from litellm.proxy.auth.oauth2_proxy_hook import handle_oauth2_proxy_request @@ -1344,6 +1351,18 @@ async def _user_api_key_auth_builder( custom_auth_api_key: bool = False try: + _lockout_reason: Final = insecure_master_key_reason( + master_key, alternative_auth_enabled=alternative_auth_enabled(general_settings) + ) + if _lockout_reason is not None: + _lockout_action: Final = master_key_lockout_action( + route=route, + method=request.method, + reason=_lockout_reason, + stored_credentials_present=stored_credentials_present(), + ) + if _lockout_action is not None: + raise master_key_lockout_exception(_lockout_action) with tracer.trace("litellm.proxy.auth.pre_db_read_auth_checks"): await pre_db_read_auth_checks( request_data=request_data, diff --git a/litellm/proxy/health_endpoints/_health_endpoints.py b/litellm/proxy/health_endpoints/_health_endpoints.py index 264a19d583d..4472a75958a 100644 --- a/litellm/proxy/health_endpoints/_health_endpoints.py +++ b/litellm/proxy/health_endpoints/_health_endpoints.py @@ -46,6 +46,7 @@ from litellm.proxy.auth.master_key_policy import ( InsecureMasterKeyReason, alternative_auth_enabled, insecure_master_key_reason, + stored_credentials_present, ) from litellm.proxy.auth.model_checks import get_key_models from litellm.proxy.auth.user_api_key_auth import user_api_key_auth @@ -1732,6 +1733,7 @@ async def _get_health_readiness_details( 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() + stored_credentials_locked: Final = insecure_master_key_reason is not None and stored_credentials_present() # check DB if prisma_client is not None: # if db passed in, check if it's connected @@ -1761,6 +1763,7 @@ async def _get_health_readiness_details( "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, + "stored_credentials_locked": stored_credentials_locked, } else: return { @@ -1775,6 +1778,7 @@ async def _get_health_readiness_details( "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, + "stored_credentials_locked": stored_credentials_locked, } except Exception as e: raise HTTPException(status_code=503, detail=f"Service Unhealthy ({e})") 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 1b7d5a086f8..f8c744f6d31 100644 --- a/tests/test_litellm/proxy/auth/test_master_key_policy.py +++ b/tests/test_litellm/proxy/auth/test_master_key_policy.py @@ -1,10 +1,13 @@ from typing import Final +import pytest + from litellm.litellm_core_utils.secret_redaction import redact_string from litellm.proxy.auth.master_key_policy import ( alternative_auth_enabled, insecure_master_key_reason, insecure_master_key_warning, + master_key_lockout_action, ) @@ -65,3 +68,80 @@ def test_insecure_master_key_warning_survives_redaction(): assert warning is not None assert "secrets.token_urlsafe" in redact_string(warning) + + +@pytest.mark.parametrize( + "route,method,expected", + [ + ("/credentials", "POST", "store_credentials"), + ("/credentials/my_creds", "PATCH", "store_credentials"), + ("/credentials/my_creds", "DELETE", "store_credentials"), + ("/model/new", "POST", "store_credentials"), + ("/model/update", "POST", "store_credentials"), + ("/model/abc-123/update", "PATCH", "store_credentials"), + ("/config/update", "POST", "store_credentials"), + ("/model/delete", "POST", None), + ("/model/block", "POST", None), + ("/model/unblock", "POST", None), + ("/credentials", "GET", "access_credentials"), + ("/credentials/by_name/my_creds", "GET", "access_credentials"), + ("/credentials/by_model", "GET", "access_credentials"), + ("/model/info", "GET", "access_credentials"), + ("/v1/model/info", "GET", "access_credentials"), + ("/v2/model/info", "GET", "access_credentials"), + ("/get/config/callbacks", "GET", "access_credentials"), + ("/config/list", "GET", "access_credentials"), + ("/config/field/info", "GET", "access_credentials"), + ("/chat/completions", "POST", "use_credentials"), + ("/v1/chat/completions", "POST", "use_credentials"), + ("/v1/embeddings", "POST", "use_credentials"), + ("/v1/messages", "POST", "use_credentials"), + ("/key/generate", "POST", "manage_virtual_keys"), + ("/key/update", "POST", "manage_virtual_keys"), + ("/key/delete", "POST", "manage_virtual_keys"), + ("/key/abc-def/regenerate", "POST", "manage_virtual_keys"), + ("/key/service-account/generate", "POST", "manage_virtual_keys"), + ("/key/block", "POST", "manage_virtual_keys"), + ("/key/info", "GET", None), + ("/key/list", "GET", None), + ("/login", "POST", None), + ("/health/readiness", "GET", None), + ("/health/readiness/details", "GET", None), + ("/models", "GET", None), + ("/v1/models", "GET", None), + ], +) +@pytest.mark.parametrize("reason", ["example_key", "missing"]) +def test_master_key_lockout_action(route, method, expected, reason): + stored: Final = expected != "store_credentials" + assert master_key_lockout_action(route, method, reason, stored_credentials_present=stored) == expected + + +@pytest.mark.parametrize( + "route,method", + [ + ("/credentials", "POST"), + ("/model/new", "POST"), + ("/credentials", "GET"), + ("/model/info", "GET"), + ("/chat/completions", "POST"), + ("/key/generate", "POST"), + ], +) +def test_master_key_lockout_action_none_when_key_secure(route, method): + assert master_key_lockout_action(route, method, None, stored_credentials_present=True) is None + + +@pytest.mark.parametrize( + "route,method", + [ + ("/credentials", "GET"), + ("/model/info", "GET"), + ("/chat/completions", "POST"), + ("/key/generate", "POST"), + ("/key/abc/regenerate", "POST"), + ], +) +@pytest.mark.parametrize("reason", ["example_key", "missing"]) +def test_master_key_lockout_action_not_blocked_without_stored_credentials(route, method, reason): + assert master_key_lockout_action(route, method, reason, stored_credentials_present=False) is None diff --git a/tests/test_litellm/proxy/auth/test_user_api_key_auth.py b/tests/test_litellm/proxy/auth/test_user_api_key_auth.py index fded86d43af..15eb3c2714b 100644 --- a/tests/test_litellm/proxy/auth/test_user_api_key_auth.py +++ b/tests/test_litellm/proxy/auth/test_user_api_key_auth.py @@ -7628,3 +7628,109 @@ async def test_claude_view_never_reinterprets_explicit_names(monkeypatch, layer) assert data["model"] == ("foo" if layer == "unclaimed" else encoded) await _normalize_claude_model(data, token, request, "/v1/messages") assert data["model"] == ("foo" if layer == "unclaimed" else encoded) +@pytest.fixture +def stored_credential(): + import litellm + from litellm.models.credentials import CredentialItem + + litellm.credential_list = [ + CredentialItem(credential_name="test-cred", credential_info={}, credential_values={"api_key": "sk-upstream"}) + ] + yield + litellm.credential_list = [] + + +def _proxy_server_attrs_for_lockout(master_key: str | None): + mock_cache = AsyncMock() + mock_cache.async_get_cache = AsyncMock(return_value=None) + mock_cache.delete_cache = MagicMock() + + mock_proxy_logging_obj = MagicMock() + mock_proxy_logging_obj.internal_usage_cache = MagicMock() + mock_proxy_logging_obj.internal_usage_cache.dual_cache = AsyncMock() + mock_proxy_logging_obj.post_call_failure_hook = AsyncMock(return_value=None) + + return { + "prisma_client": MagicMock(), + "user_api_key_cache": mock_cache, + "proxy_logging_obj": mock_proxy_logging_obj, + "master_key": master_key, + "general_settings": {}, + "llm_model_list": [], + "llm_router": None, + "open_telemetry_logger": None, + "model_max_budget_limiter": MagicMock(), + "user_custom_auth": None, + "jwt_handler": None, + "litellm_proxy_admin_name": "admin", + } + + +@pytest.mark.asyncio +async def test_insecure_master_key_locks_out_key_management_with_stored_credentials(stored_credential): + from fastapi import Request + from starlette.datastructures import URL + + import litellm.proxy.proxy_server as _proxy_server_mod + from litellm.proxy._types import ProxyException + from litellm.proxy.auth.user_api_key_auth import _user_api_key_auth_builder + + attrs = _proxy_server_attrs_for_lockout(master_key="sk-1234") + originals = {attr: getattr(_proxy_server_mod, attr, None) for attr in attrs} + try: + for attr, val in attrs.items(): + setattr(_proxy_server_mod, attr, val) + + request = Request(scope={"type": "http", "method": "POST"}) + request._url = URL(url="/key/generate") + + with pytest.raises(ProxyException) as exc_info: + await _user_api_key_auth_builder( + request=request, + api_key="Bearer sk-1234", + azure_api_key_header="", + anthropic_api_key_header=None, + google_ai_studio_api_key_header=None, + azure_apim_header=None, + request_data={}, + ) + + assert exc_info.value.code == "403" + assert "unavailable until the master key has been set" in exc_info.value.message + finally: + for attr, val in originals.items(): + setattr(_proxy_server_mod, attr, val) + + +@pytest.mark.asyncio +async def test_strong_master_key_passes_auth_on_key_management(stored_credential): + from fastapi import Request + from starlette.datastructures import URL + + import litellm.proxy.proxy_server as _proxy_server_mod + from litellm.proxy._types import LitellmUserRoles + from litellm.proxy.auth.user_api_key_auth import _user_api_key_auth_builder + + attrs = _proxy_server_attrs_for_lockout(master_key="sk-strong-random-key") + originals = {attr: getattr(_proxy_server_mod, attr, None) for attr in attrs} + try: + for attr, val in attrs.items(): + setattr(_proxy_server_mod, attr, val) + + request = Request(scope={"type": "http", "method": "POST"}) + request._url = URL(url="/key/generate") + + result = await _user_api_key_auth_builder( + request=request, + api_key="Bearer sk-strong-random-key", + azure_api_key_header="", + anthropic_api_key_header=None, + google_ai_studio_api_key_header=None, + azure_apim_header=None, + request_data={}, + ) + + assert result.user_role == LitellmUserRoles.PROXY_ADMIN + finally: + for attr, val in originals.items(): + setattr(_proxy_server_mod, attr, val) 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 228f5e0eee8..976fd6c3f8a 100644 --- a/tests/test_litellm/proxy/health_endpoints/test_health_endpoints.py +++ b/tests/test_litellm/proxy/health_endpoints/test_health_endpoints.py @@ -1417,17 +1417,20 @@ def test_health_readiness_details_reports_env_credential_login_warning(monkeypat @pytest.mark.parametrize( - "master_key, general_settings, expected_reason", + "master_key, general_settings, has_stored_credentials, expected_reason, expected_locked", [ - ("sk-1234", {}, "example_key"), - (None, {}, "missing"), - ("", {}, "missing"), - (None, {"enable_jwt_auth": True}, None), - ("sk-strong-random-key", {}, None), + ("sk-1234", {}, True, "example_key", True), + ("sk-1234", {}, False, "example_key", False), + (None, {}, True, "missing", True), + (None, {}, False, "missing", False), + ("", {}, False, "missing", False), + (None, {"enable_jwt_auth": True}, True, None, False), + ("sk-strong-random-key", {}, True, None, False), + ("sk-strong-random-key", {}, False, None, False), ], ) def test_health_readiness_details_reports_insecure_master_key_reason( - monkeypatch, master_key, general_settings, expected_reason + monkeypatch, master_key, general_settings, has_stored_credentials, expected_reason, expected_locked ): app = FastAPI() app.include_router(_health_endpoints_module.router) @@ -1437,11 +1440,20 @@ def test_health_readiness_details_reports_insecure_master_key_reason( 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) + monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", None) + monkeypatch.setattr( + litellm, + "credential_list", + [CredentialItem(credential_name="test-cred", credential_info={}, credential_values={"api_key": "sk-x"})] + if has_stored_credentials + else [], + ) response = client.get("/health/readiness/details") assert response.status_code == 200, response.text assert response.json()["insecure_master_key_reason"] == expected_reason + assert response.json()["stored_credentials_locked"] is expected_locked def test_health_readiness_allows_explicit_legacy_public_details(monkeypatch): 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 e76c894fd8f..00fe20dc05f 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/healthReadiness/useHealthReadinessDetails.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/healthReadiness/useHealthReadinessDetails.ts @@ -16,6 +16,7 @@ export interface HealthReadinessDetailsResponse { show_no_redis_warning?: boolean; show_env_credential_login_warning?: boolean; insecure_master_key_reason?: "example_key" | "missing" | null; + stored_credentials_locked?: boolean; } const fetchHealthReadinessDetails = async (accessToken: string): Promise => { diff --git a/ui/litellm-dashboard/src/components/InsecureMasterKeyWarningBanner.test.tsx b/ui/litellm-dashboard/src/components/InsecureMasterKeyWarningBanner.test.tsx index 3c6ad94c8ed..7096b4de37d 100644 --- a/ui/litellm-dashboard/src/components/InsecureMasterKeyWarningBanner.test.tsx +++ b/ui/litellm-dashboard/src/components/InsecureMasterKeyWarningBanner.test.tsx @@ -19,15 +19,29 @@ describe("InsecureMasterKeyWarningBanner", () => { 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(); + expect(screen.getByText("The master key has not been set")).toBeInTheDocument(); + expect(screen.getByText(/docs example value sk-1234/)).toBeInTheDocument(); + expect(screen.getByText(/store or use upstream credentials or manage virtual keys/)).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(); + expect(screen.getByText("The master key has not been set")).toBeInTheDocument(); + expect(screen.getByText(/store or use upstream credentials or manage virtual keys/)).toBeInTheDocument(); + }); + + it("should note stored credentials are locked when the proxy reports them", () => { + mockDetails({ status: "healthy", insecure_master_key_reason: "example_key", stored_credentials_locked: true }); + renderWithProviders(); + expect(screen.getByText(/Credentials already stored on this proxy/)).toBeInTheDocument(); + }); + + it("should not mention stored credentials when none are locked", () => { + mockDetails({ status: "healthy", insecure_master_key_reason: "missing", stored_credentials_locked: false }); + renderWithProviders(); + expect(screen.queryByText(/Credentials already stored on this proxy/)).not.toBeInTheDocument(); }); it("should render nothing when the configured key is strong", () => { diff --git a/ui/litellm-dashboard/src/components/InsecureMasterKeyWarningBanner.tsx b/ui/litellm-dashboard/src/components/InsecureMasterKeyWarningBanner.tsx index 27d6f927d47..3e5ebfe7962 100644 --- a/ui/litellm-dashboard/src/components/InsecureMasterKeyWarningBanner.tsx +++ b/ui/litellm-dashboard/src/components/InsecureMasterKeyWarningBanner.tsx @@ -4,27 +4,21 @@ 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. - - ), - }, +const REMEDIATION = ( + <> + You must set LITELLM_MASTER_KEY (or{" "} + general_settings.master_key) to a strong random key and restart the proxy before + you can store or use upstream credentials or manage virtual keys. + +); + +const BANNER_BODIES = { + example_key: ( + <> + The master key is set to the docs example value sk-1234, which does not count as set. {REMEDIATION} + + ), + missing: REMEDIATION, } as const; export const InsecureMasterKeyWarningBanner: React.FC<{ accessToken: string | null }> = ({ accessToken }) => { @@ -35,8 +29,6 @@ export const InsecureMasterKeyWarningBanner: React.FC<{ accessToken: string | nu return null; } - const { title, body } = BANNER_CONTENT[reason]; - return (
);