chore(proxy): default sensitive routes to auth

This commit is contained in:
user 2026-04-30 12:22:48 -07:00
parent b31182f62a
commit 35bbca60b0
9 changed files with 83 additions and 27 deletions

View file

@ -166,7 +166,7 @@ langfuse_default_tags: Optional[List[str]] = None
langsmith_batch_size: Optional[int] = None langsmith_batch_size: Optional[int] = None
prometheus_initialize_budget_metrics: Optional[bool] = False prometheus_initialize_budget_metrics: Optional[bool] = False
prometheus_latency_buckets: Optional[List[float]] = None prometheus_latency_buckets: Optional[List[float]] = None
require_auth_for_metrics_endpoint: Optional[bool] = False require_auth_for_metrics_endpoint: Optional[bool] = True
argilla_batch_size: Optional[int] = None argilla_batch_size: Optional[int] = None
datadog_use_v1: Optional[bool] = False # if you want to use v1 datadog logged payload. datadog_use_v1: Optional[bool] = False # if you want to use v1 datadog logged payload.
gcs_pub_sub_use_v1: Optional[bool] = ( gcs_pub_sub_use_v1: Optional[bool] = (

View file

@ -613,13 +613,8 @@ class LiteLLMRoutes(enum.Enum):
"/health/liveness", "/health/liveness",
"/test", "/test",
"/config/yaml", "/config/yaml",
"/metrics",
"/litellm/.well-known/litellm-ui-config", "/litellm/.well-known/litellm-ui-config",
"/.well-known/litellm-ui-config", "/.well-known/litellm-ui-config",
"/public/model_hub",
"/public/agent_hub",
"/public/mcp_hub",
"/public/skill_hub",
"/public/litellm_model_cost_map", "/public/litellm_model_cost_map",
] ]
) )

View file

@ -108,10 +108,12 @@ def _route_requires_auth_despite_public(
) -> bool: ) -> bool:
normalized_route = _normalize_public_auth_route(route) normalized_route = _normalize_public_auth_route(route)
if normalized_route == "/metrics": if normalized_route == "/metrics":
return litellm.require_auth_for_metrics_endpoint is True return litellm.require_auth_for_metrics_endpoint is not False
if normalized_route in _PUBLIC_AI_HUB_ROUTES: if normalized_route in _PUBLIC_AI_HUB_ROUTES:
return (general_settings or {}).get("require_auth_for_public_ai_hub") is True return (general_settings or {}).get(
"require_auth_for_public_ai_hub", True
) is True
return False return False
@ -1671,7 +1673,7 @@ async def _run_centralized_common_checks(
user_custom_auth, user_custom_auth,
) )
# Public routes (e.g. /health/liveness, /metrics) are exempt from # Public routes (e.g. /health/liveness) are exempt from
# auth in the builder — the wrapper must not retroactively apply # auth in the builder — the wrapper must not retroactively apply
# authz on top, or k8s readiness probes and other unauthenticated # authz on top, or k8s readiness probes and other unauthenticated
# callers get 401. # callers get 401.

View file

@ -19,13 +19,13 @@ class PrometheusAuthMiddleware:
""" """
Middleware to authenticate requests to the metrics endpoint. Middleware to authenticate requests to the metrics endpoint.
By default, auth is not run on the metrics endpoint. By default, auth is run on the metrics endpoint.
Enabled by setting the following in proxy_config.yaml: To allow unauthenticated metrics in proxy_config.yaml:
```yaml ```yaml
litellm_settings: litellm_settings:
require_auth_for_metrics_endpoint: true require_auth_for_metrics_endpoint: false
``` ```
""" """
@ -38,8 +38,8 @@ class PrometheusAuthMiddleware:
await self.app(scope, receive, send) await self.app(scope, receive, send)
return return
# Only run auth if configured to do so # Run auth by default; allow legacy public metrics only when explicitly disabled.
if litellm.require_auth_for_metrics_endpoint is True: if litellm.require_auth_for_metrics_endpoint is not False:
# Construct Request only when auth is actually needed # Construct Request only when auth is actually needed
request = Request(scope, receive) request = Request(scope, receive)

View file

@ -40,7 +40,7 @@ router = APIRouter()
async def public_ai_hub_auth_dependency(request: Request) -> UserAPIKeyAuth: async def public_ai_hub_auth_dependency(request: Request) -> UserAPIKeyAuth:
from litellm.proxy.proxy_server import general_settings from litellm.proxy.proxy_server import general_settings
if (general_settings or {}).get("require_auth_for_public_ai_hub") is True: if (general_settings or {}).get("require_auth_for_public_ai_hub", True) is True:
return await user_api_key_auth( return await user_api_key_auth(
request=request, request=request,
api_key=request.headers.get(SpecialHeaders.openai_authorization.value) api_key=request.headers.get(SpecialHeaders.openai_authorization.value)

View file

@ -95,7 +95,7 @@ class UISettings(BaseModel):
) )
require_auth_for_public_ai_hub: bool = Field( require_auth_for_public_ai_hub: bool = Field(
default=False, default=True,
description="If true, requires authentication for accessing the public AI Hub.", description="If true, requires authentication for accessing the public AI Hub.",
) )

View file

@ -65,6 +65,7 @@ def test_route_requires_auth_despite_public_for_metrics(monkeypatch):
def test_route_requires_auth_despite_public_for_public_ai_hub(): def test_route_requires_auth_despite_public_for_public_ai_hub():
settings = {"require_auth_for_public_ai_hub": True} settings = {"require_auth_for_public_ai_hub": True}
assert _route_requires_auth_despite_public("/public/model_hub", {}) is True
assert _route_requires_auth_despite_public("/public/model_hub", settings) is True assert _route_requires_auth_despite_public("/public/model_hub", settings) is True
assert _route_requires_auth_despite_public("/public/model_hub/", settings) is True assert _route_requires_auth_despite_public("/public/model_hub/", settings) is True
assert ( assert (
@ -74,7 +75,12 @@ def test_route_requires_auth_despite_public_for_public_ai_hub():
assert _route_requires_auth_despite_public("/public/mcp_hub", settings) is True assert _route_requires_auth_despite_public("/public/mcp_hub", settings) is True
assert _route_requires_auth_despite_public("/public/skill_hub", settings) is True assert _route_requires_auth_despite_public("/public/skill_hub", settings) is True
assert _route_requires_auth_despite_public("/public/model_hub", {}) is False assert (
_route_requires_auth_despite_public(
"/public/model_hub", {"require_auth_for_public_ai_hub": False}
)
is False
)
@pytest.mark.asyncio @pytest.mark.asyncio

View file

@ -67,7 +67,7 @@ def test_valid_auth_metrics(app_with_middleware, monkeypatch):
Test that a request to /metrics (and /metrics/) with valid auth headers passes. Test that a request to /metrics (and /metrics/) with valid auth headers passes.
""" """
# Enable auth on metrics endpoints. # Enable auth on metrics endpoints.
litellm.require_auth_for_metrics_endpoint = True monkeypatch.setattr(litellm, "require_auth_for_metrics_endpoint", True)
# Patch the auth function to simulate a valid authentication. # Patch the auth function to simulate a valid authentication.
monkeypatch.setattr( monkeypatch.setattr(
"litellm.proxy.middleware.prometheus_auth_middleware.user_api_key_auth", "litellm.proxy.middleware.prometheus_auth_middleware.user_api_key_auth",
@ -92,7 +92,7 @@ def test_invalid_auth_metrics(app_with_middleware, monkeypatch):
""" """
Test that a request to /metrics with invalid auth headers fails with a 401. Test that a request to /metrics with invalid auth headers fails with a 401.
""" """
litellm.require_auth_for_metrics_endpoint = True monkeypatch.setattr(litellm, "require_auth_for_metrics_endpoint", True)
# Patch the auth function to simulate a failed authentication. # Patch the auth function to simulate a failed authentication.
monkeypatch.setattr( monkeypatch.setattr(
"litellm.proxy.middleware.prometheus_auth_middleware.user_api_key_auth", "litellm.proxy.middleware.prometheus_auth_middleware.user_api_key_auth",
@ -126,12 +126,29 @@ def test_metrics_auth_uses_real_auth_when_route_is_public(
assert "Unauthorized access to metrics endpoint" in response.text assert "Unauthorized access to metrics endpoint" in response.text
def test_metrics_auth_is_required_by_default(app_with_middleware, monkeypatch):
"""
Metrics should require auth unless explicitly configured as public.
"""
monkeypatch.setattr(
"litellm.proxy.middleware.prometheus_auth_middleware.user_api_key_auth",
fake_invalid_auth,
)
client = TestClient(app_with_middleware)
response = client.get("/metrics")
assert response.status_code == 401, response.text
assert "Unauthorized access to metrics endpoint" in response.text
def test_no_auth_metrics_when_disabled(app_with_middleware, monkeypatch): def test_no_auth_metrics_when_disabled(app_with_middleware, monkeypatch):
""" """
Test that when require_auth_for_metrics_endpoint is False, requests to /metrics Test that when require_auth_for_metrics_endpoint is False, requests to /metrics
bypass the auth check. bypass the auth check.
""" """
litellm.require_auth_for_metrics_endpoint = False monkeypatch.setattr(litellm, "require_auth_for_metrics_endpoint", False)
# To ensure auth is not run, patch the auth function with one that will raise if called. # To ensure auth is not run, patch the auth function with one that will raise if called.
def should_not_be_called(*args, **kwargs): def should_not_be_called(*args, **kwargs):
@ -148,11 +165,11 @@ def test_no_auth_metrics_when_disabled(app_with_middleware, monkeypatch):
assert response.json() == {"msg": "metrics OK"} assert response.json() == {"msg": "metrics OK"}
def test_non_metrics_requests_pass_through(app_with_middleware): def test_non_metrics_requests_pass_through(app_with_middleware, monkeypatch):
""" """
Test that non-metrics endpoints pass through the middleware unaffected. Test that non-metrics endpoints pass through the middleware unaffected.
""" """
litellm.require_auth_for_metrics_endpoint = True monkeypatch.setattr(litellm, "require_auth_for_metrics_endpoint", True)
client = TestClient(app_with_middleware) client = TestClient(app_with_middleware)
@ -170,7 +187,7 @@ def test_non_metrics_requests_dont_trigger_auth(app_with_middleware, monkeypatch
Test that non-metrics requests never trigger auth, even when auth is enabled Test that non-metrics requests never trigger auth, even when auth is enabled
and the auth function would reject the request. and the auth function would reject the request.
""" """
litellm.require_auth_for_metrics_endpoint = True monkeypatch.setattr(litellm, "require_auth_for_metrics_endpoint", True)
def should_not_be_called(*args, **kwargs): def should_not_be_called(*args, **kwargs):
raise Exception("Auth should not be called for non-metrics requests") raise Exception("Auth should not be called for non-metrics requests")

View file

@ -12,7 +12,7 @@ from fastapi.testclient import TestClient
from starlette.datastructures import URL from starlette.datastructures import URL
from starlette.requests import Request from starlette.requests import Request
from litellm.proxy._types import ProxyException from litellm.proxy._types import LitellmUserRoles, ProxyException
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.proxy.public_endpoints import router from litellm.proxy.public_endpoints import router
from litellm.proxy.public_endpoints.public_endpoints import ( from litellm.proxy.public_endpoints.public_endpoints import (
@ -97,7 +97,7 @@ def test_get_litellm_model_cost_map_returns_cost_map():
) )
def test_public_ai_hub_info_stays_public_by_default(monkeypatch): def test_public_ai_hub_info_requires_auth_by_default(monkeypatch):
app = FastAPI() app = FastAPI()
app.include_router(router) app.include_router(router)
client = TestClient(app) client = TestClient(app)
@ -105,16 +105,31 @@ def test_public_ai_hub_info_stays_public_by_default(monkeypatch):
monkeypatch.setattr("litellm.proxy.proxy_server.general_settings", {}) monkeypatch.setattr("litellm.proxy.proxy_server.general_settings", {})
monkeypatch.setattr("litellm.proxy.proxy_server.master_key", "sk-master") monkeypatch.setattr("litellm.proxy.proxy_server.master_key", "sk-master")
with pytest.raises(ProxyException):
client.get("/public/model_hub/info")
def test_public_ai_hub_info_can_be_explicitly_public(monkeypatch):
app = FastAPI()
app.include_router(router)
client = TestClient(app)
monkeypatch.setattr(
"litellm.proxy.proxy_server.general_settings",
{"require_auth_for_public_ai_hub": False},
)
monkeypatch.setattr("litellm.proxy.proxy_server.master_key", "sk-master")
response = client.get("/public/model_hub/info") response = client.get("/public/model_hub/info")
assert response.status_code == 200, response.text assert response.status_code == 200, response.text
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_public_ai_hub_info_requires_auth_when_configured(monkeypatch): async def test_public_ai_hub_info_requires_auth_by_default_dependency(monkeypatch):
monkeypatch.setattr( monkeypatch.setattr(
"litellm.proxy.proxy_server.general_settings", "litellm.proxy.proxy_server.general_settings",
{"require_auth_for_public_ai_hub": True}, {},
) )
monkeypatch.setattr("litellm.proxy.proxy_server.master_key", "sk-master") monkeypatch.setattr("litellm.proxy.proxy_server.master_key", "sk-master")
request = Request( request = Request(
@ -131,6 +146,27 @@ async def test_public_ai_hub_info_requires_auth_when_configured(monkeypatch):
await public_ai_hub_auth_dependency(request) await public_ai_hub_auth_dependency(request)
@pytest.mark.asyncio
async def test_public_ai_hub_info_skips_auth_when_explicitly_disabled(monkeypatch):
monkeypatch.setattr(
"litellm.proxy.proxy_server.general_settings",
{"require_auth_for_public_ai_hub": False},
)
request = Request(
scope={
"type": "http",
"method": "GET",
"path": "/public/model_hub/info",
"headers": [],
}
)
request._url = URL(url="/public/model_hub/info")
auth = await public_ai_hub_auth_dependency(request)
assert auth.user_role == LitellmUserRoles.INTERNAL_USER_VIEW_ONLY
def test_watsonx_provider_fields(): def test_watsonx_provider_fields():
"""Test that Watsonx provider has all required credential fields including multiple auth options.""" """Test that Watsonx provider has all required credential fields including multiple auth options."""
app = FastAPI() app = FastAPI()