chore(auth): substitute alias for master key on UserAPIKeyAuth

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) <noreply@anthropic.com>
This commit is contained in:
user
2026-04-29 18:53:12 +00:00
co-authored by Claude Opus 4.7
parent 9b3cd5ca25
commit 9d9f09934e
5 changed files with 67 additions and 25 deletions
+4
View File
@@ -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")
+6 -1
View File
@@ -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,
@@ -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", "")
@@ -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)
@@ -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