From 9d9f09934ec09da0ae95d8f6cd962d604258d1ee Mon Sep 17 00:00:00 2001 From: user <70670632+stuxf@users.noreply.github.com> Date: Sat, 25 Apr 2026 03:44:00 +0000 Subject: [PATCH] chore(auth): substitute alias for master key on UserAPIKeyAuth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related changes to how the master-key auth path interacts with downstream consumers of UserAPIKeyAuth.api_key: 1. The master-key auth branch in user_api_key_auth.py now sets `valid_token.api_key` to a stable alias (`LITELLM_PROXY_MASTER_KEY_ALIAS = "litellm_proxy_master_key"`) instead of the raw master key. Downstream consumers — spend logging, Prometheus metrics, audit trails, rate limiting, cost tracking — now receive the alias instead of the master key (which they would previously hash and propagate). Neither the raw master key nor its hash flows past the auth layer. 2. `_is_master_key` in spend_tracking_utils.py is reduced to a strict raw-only constant-time comparison. The hashed form is no longer considered equivalent. Side effects: - The two hash-detection blocks in `get_logging_payload` are removed. They were re-detecting the master key per spend-log write to swap in the alias; that detection happens once at the auth layer now. - The `disable_adding_master_key_hash_to_db` general setting becomes a no-op. Operators can remove it from their config; existing config is still accepted. - Operator dashboards that filter Prometheus metrics by the master-key hash will need to switch to the `api_key="litellm_proxy_master_key"` label. Co-Authored-By: Claude Opus 4.7 (1M context) --- litellm/constants.py | 4 ++ litellm/proxy/auth/user_api_key_auth.py | 7 ++- .../spend_tracking/spend_tracking_utils.py | 27 ++--------- .../proxy/auth/test_user_api_key_auth.py | 46 +++++++++++++++++++ .../test_spend_tracking_utils.py | 8 +++- 5 files changed, 67 insertions(+), 25 deletions(-) diff --git a/litellm/constants.py b/litellm/constants.py index a0e99dd16b..d78c124d71 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -1393,6 +1393,10 @@ except (ValueError, TypeError): LITTELM_INTERNAL_HEALTH_SERVICE_ACCOUNT_NAME = "litellm-internal-health-check" LITTELM_CLI_SERVICE_ACCOUNT_NAME = "litellm-cli" LITELLM_INTERNAL_JOBS_SERVICE_ACCOUNT_NAME = "litellm_internal_jobs" +# Stable identifier substituted in place of the master key on UserAPIKeyAuth +# objects so the master key (or its hash) never propagates to spend logs, +# Prometheus metrics, audit trails, or any other downstream consumer. +LITELLM_PROXY_MASTER_KEY_ALIAS = "litellm_proxy_master_key" # Key Rotation Constants LITELLM_KEY_ROTATION_ENABLED = os.getenv("LITELLM_KEY_ROTATION_ENABLED", "false") diff --git a/litellm/proxy/auth/user_api_key_auth.py b/litellm/proxy/auth/user_api_key_auth.py index b8db3cd2a7..f0c2a4514f 100644 --- a/litellm/proxy/auth/user_api_key_auth.py +++ b/litellm/proxy/auth/user_api_key_auth.py @@ -21,6 +21,7 @@ import litellm from litellm._logging import verbose_logger, verbose_proxy_logger from litellm._service_logger import ServiceLogging from litellm.caching import DualCache +from litellm.constants import LITELLM_PROXY_MASTER_KEY_ALIAS from litellm.litellm_core_utils.dd_tracing import tracer from litellm.litellm_core_utils.dot_notation_indexing import get_nested_value from litellm.proxy._types import * @@ -1119,10 +1120,14 @@ async def _user_api_key_auth_builder( # noqa: PLR0915 ) if is_master_key_valid: + # Substitute a stable alias for the raw master key so neither the + # master key nor its hash propagates into spend logs, Prometheus + # /metrics labels, audit trails, rate-limit buckets, or any other + # downstream consumer of UserAPIKeyAuth.api_key. _user_api_key_obj = await _return_user_api_key_auth_obj( user_obj=None, user_role=LitellmUserRoles.PROXY_ADMIN, - api_key=master_key, + api_key=LITELLM_PROXY_MASTER_KEY_ALIAS, parent_otel_span=parent_otel_span, valid_token_dict={ **end_user_params, diff --git a/litellm/proxy/spend_tracking/spend_tracking_utils.py b/litellm/proxy/spend_tracking/spend_tracking_utils.py index 889c781004..36ed16e026 100644 --- a/litellm/proxy/spend_tracking/spend_tracking_utils.py +++ b/litellm/proxy/spend_tracking/spend_tracking_utils.py @@ -53,20 +53,13 @@ def _get_max_string_length_prompt_in_db() -> int: def _is_master_key(api_key: Optional[str], _master_key: Optional[str]) -> bool: + """ + Raw-only constant-time master-key comparison. The hashed form is never + considered equivalent — only the raw master-key string matches. + """ if _master_key is None or api_key is None: return False - - ## string comparison - is_master_key = secrets.compare_digest(api_key, _master_key) - if is_master_key: - return True - - ## hash comparison - is_master_key = secrets.compare_digest(api_key, hash_token(_master_key)) - if is_master_key: - return True - - return False + return secrets.compare_digest(api_key, _master_key) def _get_spend_logs_metadata( @@ -295,11 +288,6 @@ def get_logging_payload( # noqa: PLR0915 if api_key.startswith("sk-"): # hash the api_key api_key = hash_token(api_key) - if ( - _is_master_key(api_key=api_key, _master_key=master_key) - and general_settings.get("disable_adding_master_key_hash_to_db") is True - ): - api_key = "litellm_proxy_master_key" # use a known alias, if the user disabled storing master key in db if ( standard_logging_payload is not None @@ -324,11 +312,6 @@ def get_logging_payload( # noqa: PLR0915 and standard_logging_payload.get("request_tags") is not None ): # use 'tags' from standard logging payload instead request_tags = json.dumps(standard_logging_payload["request_tags"]) - if ( - _is_master_key(api_key=api_key, _master_key=master_key) - and general_settings.get("disable_adding_master_key_hash_to_db") is True - ): - api_key = "litellm_proxy_master_key" # use a known alias, if the user disabled storing master key in db _model_id = metadata.get("model_info", {}).get("id", "") _model_group = metadata.get("model_group", "") 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 9c43ebcbe7..08f4bd0ebf 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 @@ -2581,3 +2581,49 @@ async def test_centralized_common_checks_user_http_exception_isolates_to_user_on finally: for k, v in originals.items(): setattr(_proxy_server_mod, k, v) + + +@pytest.mark.asyncio +async def test_master_key_auth_substitutes_alias_for_api_key(): + """ + When the master key authenticates a request, the resulting + ``UserAPIKeyAuth.api_key`` must be the stable alias + ``LITELLM_PROXY_MASTER_KEY_ALIAS`` — never the raw master key (which + would propagate downstream and be hashed into spend logs, Prometheus + ``/metrics`` labels, or audit trails) and never the master-key hash. + """ + from fastapi import Request + from starlette.datastructures import URL + + from litellm.constants import LITELLM_PROXY_MASTER_KEY_ALIAS + from litellm.proxy.auth.user_api_key_auth import _user_api_key_auth_builder + from litellm.proxy.utils import hash_token + + import litellm.proxy.proxy_server as _proxy_server_mod + + attrs = _proxy_server_attrs_for_custom_auth(user_custom_auth=None) + master_key = attrs["master_key"] + _orig = {k: getattr(_proxy_server_mod, k, None) for k in attrs} + try: + for k, v in attrs.items(): + setattr(_proxy_server_mod, k, v) + + request = Request(scope={"type": "http"}) + request._url = URL(url="/chat/completions") + + result = await _user_api_key_auth_builder( + request=request, + api_key=f"Bearer {master_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.api_key == LITELLM_PROXY_MASTER_KEY_ALIAS + assert result.api_key != master_key + assert result.api_key != hash_token(master_key) + finally: + for k, v in _orig.items(): + setattr(_proxy_server_mod, k, v) diff --git a/tests/test_litellm/proxy/spend_tracking/test_spend_tracking_utils.py b/tests/test_litellm/proxy/spend_tracking/test_spend_tracking_utils.py index e532b948c7..185d337f90 100644 --- a/tests/test_litellm/proxy/spend_tracking/test_spend_tracking_utils.py +++ b/tests/test_litellm/proxy/spend_tracking/test_spend_tracking_utils.py @@ -1513,9 +1513,13 @@ class TestIsMasterKey: def test_non_matching_key_returns_false(self): assert _is_master_key(api_key="sk-other", _master_key="sk-master") is False - def test_hashed_key_returns_true(self): + def test_master_key_hash_is_rejected(self): + """ + ``_is_master_key`` must not accept ``hash_token(master_key)`` as + equivalent to the raw master key — only the raw value matches. + """ from litellm.proxy.utils import hash_token master = "sk-master-key-123" hashed = hash_token(master) - assert _is_master_key(api_key=hashed, _master_key=master) is True + assert _is_master_key(api_key=hashed, _master_key=master) is False