From 35bbca60b057069ed71c71e1c35877eee6cde43f Mon Sep 17 00:00:00 2001 From: user <70670632+stuxf@users.noreply.github.com> Date: Thu, 30 Apr 2026 12:22:48 -0700 Subject: [PATCH] chore(proxy): default sensitive routes to auth --- litellm/__init__.py | 2 +- litellm/proxy/_types.py | 5 --- litellm/proxy/auth/user_api_key_auth.py | 8 ++-- .../middleware/prometheus_auth_middleware.py | 10 ++--- .../public_endpoints/public_endpoints.py | 2 +- .../proxy_setting_endpoints.py | 2 +- .../proxy/auth/test_user_api_key_auth.py | 8 +++- .../test_prometheus_auth_middleware.py | 29 +++++++++--- .../public_endpoints/test_public_endpoints.py | 44 +++++++++++++++++-- 9 files changed, 83 insertions(+), 27 deletions(-) diff --git a/litellm/__init__.py b/litellm/__init__.py index 77fa48625d..f41f976fe8 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -166,7 +166,7 @@ langfuse_default_tags: Optional[List[str]] = None langsmith_batch_size: Optional[int] = None prometheus_initialize_budget_metrics: Optional[bool] = False 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 datadog_use_v1: Optional[bool] = False # if you want to use v1 datadog logged payload. gcs_pub_sub_use_v1: Optional[bool] = ( diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index e3b7b3a116..d0fb70e662 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -613,13 +613,8 @@ class LiteLLMRoutes(enum.Enum): "/health/liveness", "/test", "/config/yaml", - "/metrics", "/litellm/.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", ] ) diff --git a/litellm/proxy/auth/user_api_key_auth.py b/litellm/proxy/auth/user_api_key_auth.py index 49cc1b3b2c..d0319e06c5 100644 --- a/litellm/proxy/auth/user_api_key_auth.py +++ b/litellm/proxy/auth/user_api_key_auth.py @@ -108,10 +108,12 @@ def _route_requires_auth_despite_public( ) -> bool: normalized_route = _normalize_public_auth_route(route) 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: - 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 @@ -1671,7 +1673,7 @@ async def _run_centralized_common_checks( 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 # authz on top, or k8s readiness probes and other unauthenticated # callers get 401. diff --git a/litellm/proxy/middleware/prometheus_auth_middleware.py b/litellm/proxy/middleware/prometheus_auth_middleware.py index 529076ea47..513ecd1bc8 100644 --- a/litellm/proxy/middleware/prometheus_auth_middleware.py +++ b/litellm/proxy/middleware/prometheus_auth_middleware.py @@ -19,13 +19,13 @@ class PrometheusAuthMiddleware: """ 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 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) return - # Only run auth if configured to do so - if litellm.require_auth_for_metrics_endpoint is True: + # Run auth by default; allow legacy public metrics only when explicitly disabled. + if litellm.require_auth_for_metrics_endpoint is not False: # Construct Request only when auth is actually needed request = Request(scope, receive) diff --git a/litellm/proxy/public_endpoints/public_endpoints.py b/litellm/proxy/public_endpoints/public_endpoints.py index dceee41819..827a59b2e0 100644 --- a/litellm/proxy/public_endpoints/public_endpoints.py +++ b/litellm/proxy/public_endpoints/public_endpoints.py @@ -40,7 +40,7 @@ router = APIRouter() async def public_ai_hub_auth_dependency(request: Request) -> UserAPIKeyAuth: 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( request=request, api_key=request.headers.get(SpecialHeaders.openai_authorization.value) diff --git a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py index da500252d7..a5cf52b071 100644 --- a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py +++ b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py @@ -95,7 +95,7 @@ class UISettings(BaseModel): ) require_auth_for_public_ai_hub: bool = Field( - default=False, + default=True, description="If true, requires authentication for accessing the public AI Hub.", ) 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 ae5d01dc2a..0cce501a15 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 @@ -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(): 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 ( @@ -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/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 diff --git a/tests/test_litellm/proxy/middleware/test_prometheus_auth_middleware.py b/tests/test_litellm/proxy/middleware/test_prometheus_auth_middleware.py index d280425030..4bc707209a 100644 --- a/tests/test_litellm/proxy/middleware/test_prometheus_auth_middleware.py +++ b/tests/test_litellm/proxy/middleware/test_prometheus_auth_middleware.py @@ -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. """ # 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. monkeypatch.setattr( "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. """ - 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. monkeypatch.setattr( "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 +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): """ Test that when require_auth_for_metrics_endpoint is False, requests to /metrics 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. 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"} -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. """ - litellm.require_auth_for_metrics_endpoint = True + monkeypatch.setattr(litellm, "require_auth_for_metrics_endpoint", True) 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 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): raise Exception("Auth should not be called for non-metrics requests") diff --git a/tests/test_litellm/proxy/public_endpoints/test_public_endpoints.py b/tests/test_litellm/proxy/public_endpoints/test_public_endpoints.py index 68ff88d60f..f39e39c565 100644 --- a/tests/test_litellm/proxy/public_endpoints/test_public_endpoints.py +++ b/tests/test_litellm/proxy/public_endpoints/test_public_endpoints.py @@ -12,7 +12,7 @@ from fastapi.testclient import TestClient from starlette.datastructures import URL 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.public_endpoints import router 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.include_router(router) 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.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") assert response.status_code == 200, response.text @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( "litellm.proxy.proxy_server.general_settings", - {"require_auth_for_public_ai_hub": True}, + {}, ) monkeypatch.setattr("litellm.proxy.proxy_server.master_key", "sk-master") 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) +@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(): """Test that Watsonx provider has all required credential fields including multiple auth options.""" app = FastAPI()