BerriAI/litellm · error · HTTPException
Only proxy admins can set `allowed_passthrough_routes` on a
Error message
Only proxy admins can set `allowed_passthrough_routes` on a {entity}. What it means
_check_passthrough_routes_caller_permission runs on key and team create/update paths: only role PROXY_ADMIN may set allowed_passthrough_routes, either as a top-level field or under metadata (both variants raise, with slightly different messages). The field is privileged because it short-circuits the role-based route gate — a non-admin key or team granting itself passthrough routes would escalate its own permissions, so keys and teams are gated identically. PROXY_ADMIN_VIEW_ONLY is intentionally excluded (and is blocked from writes upstream anyway).
Source
Thrown at litellm/proxy/management_endpoints/common_utils.py:123
return user_api_key_dict.user_id
def _check_passthrough_routes_caller_permission(
data: BaseModel,
user_api_key_dict: UserAPIKeyAuth,
*,
entity: str = "key",
) -> None:
"""
Only proxy admins may set `allowed_passthrough_routes` (top-level or under
`metadata`) — it short-circuits the role-based route gate, so keys and teams
must be gated identically.
"""
# view-only admins excluded by design; blocked upstream from writes anyway
if user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value:
return
if getattr(data, "allowed_passthrough_routes", None):
raise HTTPException(
status_code=403,
detail={"error": f"Only proxy admins can set `allowed_passthrough_routes` on a {entity}."},
)
metadata: Final = getattr(data, "metadata", None)
if isinstance(metadata, dict) and metadata.get("allowed_passthrough_routes"):
raise HTTPException(
status_code=403,
detail={"error": f"Only proxy admins can set `metadata.allowed_passthrough_routes` on a {entity}."},
)
def _is_user_team_admin(user_api_key_dict: UserAPIKeyAuth, team_obj: LiteLLM_TeamTable) -> bool:
for member in team_obj.members_with_roles:
if (member.user_id is not None and member.user_id == user_api_key_dict.user_id) and member.role == "admin":
return True
return False
View on GitHub (pinned to 77b7c6c40c)
Solutions
- Remove allowed_passthrough_routes (both top-level and from metadata) from the payload when acting as a non-admin
- Or have a proxy admin make the change (master key / proxy_admin-owned key), possibly defining routes globally in config instead of per-key
- Audit payloads that template admin keys — strip privileged fields before reusing them for lower-privilege creates
Example fix
# before: team-admin key trying to open passthrough routes
curl -X POST http://localhost:4000/key/generate -H "Authorization: Bearer sk-team-admin" \
-d '{"team_id": "t1", "allowed_passthrough_routes": ["/v1/embeddings"]}'
# 403 Only proxy admins can set `allowed_passthrough_routes` on a key.
# after: strip the field (non-admin) or use the master key (admin)
curl -X POST http://localhost:4000/key/generate -H "Authorization: Bearer sk-team-admin" -d '{"team_id": "t1"}'
curl -X POST http://localhost:4000/key/generate -H "Authorization: Bearer $LITELLM_MASTER_KEY" \
-d '{"team_id": "t1", "allowed_passthrough_routes": ["/v1/embeddings"]}' Defensive patterns
Strategy: validation
Validate before calling
PRIVILEGED_FIELDS = {"allowed_passthrough_routes"}
def strip_privileged_fields(payload: dict, is_admin: bool) -> dict:
"""Remove fields a non-admin caller must not set (top-level and metadata)."""
if is_admin:
return payload
cleaned = {k: v for k, v in payload.items() if k not in PRIVILEGED_FIELDS}
md = cleaned.get("metadata")
if isinstance(md, dict):
cleaned["metadata"] = {k: v for k, v in md.items() if k not in PRIVILEGED_FIELDS}
return cleaned Type guard
from typing import TypeGuard
PRIVILEGED_FIELDS = frozenset({"allowed_passthrough_routes"})
def is_safe_non_admin_payload(payload: dict) -> TypeGuard[dict]:
"""True when no privileged passthrough fields appear at any level."""
if any(k in PRIVILEGED_FIELDS for k in payload):
return False
md = payload.get("metadata")
return not (isinstance(md, dict) and any(k in PRIVILEGED_FIELDS for k in md)) Try / catch
import httpx
try:
r = httpx.post(f"{PROXY_URL}/key/generate", json=payload, headers=hdrs)
r.raise_for_status()
except httpx.HTTPStatusError as e:
if e.response.status_code == 403 and "allowed_passthrough_routes" in e.response.text:
# do not retry as-is: strip the field or escalate to an admin key
raise PermissionError(
"passthrough routes are admin-only; remove the field or use an admin key"
) from e
raise Prevention
- Sanitize key/team payloads before reuse: strip allowed_passthrough_routes from templates
- Define passthrough routes globally in the proxy config instead of per-key where possible
- Keep a separate admin path for permission-changing operations; never expose the field in self-service UIs
When it happens
Trigger: POST /key/generate, /key/update, /team/new, or /team/update issued by an internal_user or team admin that includes allowed_passthrough_routes at the top level or nested as metadata.allowed_passthrough_routes; self-service flows where users configure their own keys' passthrough list.
Common situations: Delegated team admins trying to open passthrough routes for their team's keys; automation copying an admin key's payload as a template for non-admin keys; frontends exposing the field to all users in the key editor.
Related errors
- max_budget ({_requested_max_budget}) cannot exceed the calle
- Key not found: {hashed_token}
- Only proxy admins, team admins, or org admins can call {rout
- Unable to record skill ownership: caller has no identity sco
- Skill not found: {skill_id}
AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18).
Data as JSON: /api/errors/dd37a9de5d0ecb73.
Report an issue: GitHub.