Skip to content

fix(proxy): run policy checks on config pass-through entries that omit auth - #43250

Open
mateo-berri wants to merge 3 commits into
mainfrom
litellm_passthrough_omitted_auth_policy_checks
Open

mateo-berri wants to merge 3 commits into
mainfrom
litellm_passthrough_omitted_auth_policy_checks

Conversation

@mateo-berri

@mateo-berri mateo-berri commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • A config pass-through entry that omits auth skips every policy check after key auth
  • A key past its team budget still reaches the upstream through such an entry
  • Only auth: true runs the checks, and it also demands an allowed_passthrough_routes grant
  • Nothing at boot tells the admin an entry omitted auth

How it solves it:

  • Omitted auth now means any valid key, with the full policy checks
  • auth: true still needs a route grant, auth: false stays public
  • Config and DB entries read auth with the same bool parsing (yes, 0, "true")
  • With a DB, config entries no longer come back as auth: true copies
  • Boot warns once per process for every entry that omits auth

Intentional product change: an entry that omits auth now runs team, org, and end-user budgets, guardrails, model access, and team blocks on every caller, so a key those checks refuse on /v1/chat/completions is refused here with the same error. A key scoped to allowed_routes: ["llm_api_routes"] now reaches such an entry instead of getting a 403 about allowed_passthrough_routes. There is no longer a way to require a key and skip the checks: auth: false serves the entry without a key

User Flow

Before: a key on an over-budget team is refused on chat completions but sails through a config pass-through entry that omits auth

  1. The proxy admin adds /echo -> https://postman-echo.com/get under general_settings.pass_through_endpoints with no auth key and restarts the proxy
  2. A developer whose team is over budget sends POST https://litellm-domain/v1/chat/completions and gets 422 Budget has been exceeded! Team=...
  3. The same developer sends GET https://litellm-domain/echo with the same key and gets 200 with the upstream's JSON
  4. The boot log says nothing about the entry, so the admin has no hint it skips budgets, guardrails, model access, and team blocks
  5. Any key past a team, org, or end-user budget, on a blocked team, or barred from a model keeps calling /echo until its own key budget runs out

After: the same key gets the same 422 on /echo, and the boot log names the entry

  1. The proxy admin adds /echo -> https://postman-echo.com/get under general_settings.pass_through_endpoints with no auth key and restarts the proxy
  2. A developer whose team is over budget sends POST https://litellm-domain/v1/chat/completions and gets 422 Budget has been exceeded! Team=...
  3. The same developer sends GET https://litellm-domain/echo with the same key and gets 422 Budget has been exceeded! Team=..., the chat completions error
  4. The boot log prints pass_through_endpoints entry '/echo' sets no \auth`: any valid LiteLLM key may call it ...once per worker, namingauth: trueandauth: false`
  5. Keys those checks refuse on chat completions are refused on /echo the same way, and a key scoped to allowed_routes: ["llm_api_routes"] gets 200 on /echo instead of a 403

Relevant issues

Related to #36508: an entry that omits auth now counts as an LLM API route, so a key scoped to llm_api_routes reaches it instead of getting a 403

Linear ticket

Resolves LIT-8631

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*, make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Screenshots / Proof of Fix

Shared setup for both legs. Each leg boots two proxy instances with --num_workers 2 from the same config, sharing one Postgres, and the cases alternate between the two instances (the port is in every command). The chat completions calls hit the real openai/gpt-5-mini and cost real money; they are how the team's spend gets past its budget

model_list:
  - model_name: gpt-5-mini
    litellm_params:
      model: openai/gpt-5-mini
      api_key: os.environ/OPENAI_API_KEY
general_settings:
  store_model_in_db: true
  pass_through_endpoints:
    - path: "/echo"
      target: "https://postman-echo.com/get"
      headers:
        x-lit8631: "repro-omitted"
    - path: "/echo-auth-true"
      target: "https://postman-echo.com/get"
      auth: true
      headers:
        x-lit8631: "repro-auth-true"
    - path: "/echo-auth-false"
      target: "https://postman-echo.com/get"
      auth: false
      headers:
        x-lit8631: "repro-auth-false"

Keys and the team, created once with the master key and reused by both legs (the DB is shared):

# <key-plain>: a normal key with no route grants
curl -s http://127.0.0.1:$PORT/key/generate -H "Authorization: Bearer $MASTER" -H 'Content-Type: application/json' -d '{"key_alias":"lit8631-plain"}'
# a team one chat call pushes past its budget, and <key-team-over-budget> on it, granted /echo-auth-true
curl -s http://127.0.0.1:$PORT/team/new -H "Authorization: Bearer $MASTER" -H 'Content-Type: application/json' -d '{"max_budget":0.000001}'
curl -s http://127.0.0.1:$PORT/key/generate -H "Authorization: Bearer $MASTER" -H 'Content-Type: application/json' -d '{"team_id":"<team-id>","key_alias":"teamtiny-key","allowed_passthrough_routes":["/echo-auth-true"]}'
# one chat completion on that key, then ~20s for the spend write: team spend 3.4e-05 > max_budget 1e-06
# <key-llm-api-routes>: scoped to the llm_api_routes group only
curl -s http://127.0.0.1:$PORT/key/generate -H "Authorization: Bearer $MASTER" -H 'Content-Type: application/json' -d '{"key_alias":"lit8631-llm-api-routes","allowed_routes":["llm_api_routes"]}'

Before (cd1107a)

plain key: POST /v1/chat/completions (real provider call)

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' -H 'Authorization: Bearer <key-plain>' http://127.0.0.1:21767/v1/chat/completions -H 'Content-Type: application/json' -d '{"model":"gpt-5-mini","messages":[{"role":"user","content":"say hi"}],"max_completion_tokens":16}'
  2. Observed

    {"id":"chatcmpl-ESA1A3qaR0NH7kGlJquABv9VDifuH","created":1790380400,"model":"gpt-5-mini","object":"chat.completion","choices":[{"finish_reason":"length","index":0,"message":{"content":"","role":"assistant","provider_specific_fields":{"refusal":null},"annotations":[]},"provider_specific_fields":{}}],"usage":{"completion_tokens":16,"prompt_tokens":8,"total_tokens":24,"completion_tokens_details":{"accepted_prediction_tokens":0,"audio_tokens":0,"reasoning_tokens":16,"rejected_prediction_tokens":0},"prompt_tokens_details":{"audio_tokens":0,"cached_tokens":0}},"service_tier":"default"}
    HTTP 200
    

no key: GET /echo (auth omitted)

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' http://127.0.0.1:32759/echo
  2. Observed

    {"error":{"message":"Authentication Error, No api key passed in.","type":"auth_error","param":"None","code":"401"}}
    HTTP 401
    

plain key: GET /echo (auth omitted)

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' -H 'Authorization: Bearer <key-plain>' http://127.0.0.1:21767/echo
  2. Observed

    {"args":{},"headers":{"host":"postman-echo.com","accept-encoding":"gzip, br","accept":"*/*","x-lit8631":"repro-omitted","user-agent":"litellm/1.104.0","x-forwarded-proto":"https"},"url":"https://postman-echo.com/get"}
    HTTP 200
    

team-over-budget key: POST /v1/chat/completions control

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' -H 'Authorization: Bearer <key-team-over-budget>' http://127.0.0.1:32759/v1/chat/completions -H 'Content-Type: application/json' -d '{"model":"gpt-5-mini","messages":[{"role":"user","content":"say hi"}],"max_completion_tokens":16}'
  2. Observed

    {"error":{"message":"Budget has been exceeded! Team=0cceec4d-a1cf-4fd1-bea5-57d9e4c9c9d1 Current cost: 3.4e-05, Max budget: 1e-06","type":"budget_exceeded","param":null,"code":"422"}}
    HTTP 422
    

team-over-budget key: GET /echo (auth omitted)

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' -H 'Authorization: Bearer <key-team-over-budget>' http://127.0.0.1:21767/echo
  2. Observed

    {"args":{},"headers":{"host":"postman-echo.com","accept-encoding":"gzip, br","accept":"*/*","x-lit8631":"repro-omitted","user-agent":"litellm/1.104.0","x-forwarded-proto":"https"},"url":"https://postman-echo.com/get"}
    HTTP 200
    

team-over-budget key: GET /echo (auth omitted), other instance

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' -H 'Authorization: Bearer <key-team-over-budget>' http://127.0.0.1:32759/echo
  2. Observed

    {"args":{},"headers":{"host":"postman-echo.com","accept-encoding":"gzip, br","accept":"*/*","x-lit8631":"repro-omitted","user-agent":"litellm/1.104.0","x-forwarded-proto":"https"},"url":"https://postman-echo.com/get"}
    HTTP 200
    

team-over-budget key, granted the route: GET /echo-auth-true (auth: true)

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' -H 'Authorization: Bearer <key-team-over-budget>' http://127.0.0.1:21767/echo-auth-true
  2. Observed

    {"error":{"message":"Budget has been exceeded! Team=0cceec4d-a1cf-4fd1-bea5-57d9e4c9c9d1 Current cost: 3.4e-05, Max budget: 1e-06","type":"budget_exceeded","param":null,"code":"422"}}
    HTTP 422
    

no key: GET /echo-auth-false (auth: false)

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' http://127.0.0.1:32759/echo-auth-false
  2. Observed

    {"args":{},"headers":{"host":"postman-echo.com","accept-encoding":"gzip, br","accept":"*/*","x-lit8631":"repro-auth-false","user-agent":"litellm/1.104.0","x-forwarded-proto":"https"},"url":"https://postman-echo.com/get"}
    HTTP 200
    

llm_api_routes-only key: GET /echo (auth omitted)

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' -H 'Authorization: Bearer <key-llm-api-routes>' http://127.0.0.1:21767/echo
  2. Observed

    {"detail":"Key/team not allowed to access passthrough route /echo. Configure `allowed_passthrough_routes` on the team or key."}
    HTTP 403
    

llm_api_routes-only key: GET /echo-auth-true (auth: true)

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' -H 'Authorization: Bearer <key-llm-api-routes>' http://127.0.0.1:32759/echo-auth-true
  2. Observed

    {"detail":"Key/team not allowed to access passthrough route /echo-auth-true. Configure `allowed_passthrough_routes` on the team or key."}
    HTTP 403
    

plain key, no grant: GET /echo-auth-true (auth: true)

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' -H 'Authorization: Bearer <key-plain>' http://127.0.0.1:21767/echo-auth-true
  2. Observed

    {"error":{"message":"Key/team not allowed to access passthrough route /echo-auth-true. Configure `allowed_passthrough_routes` on the team or key.","type":"auth_error","param":"None","code":"403"}}
    HTTP 403
    

boot log: warning for the entry that omits auth

  1. Run, on each instance's boot log

    grep -c 'sets no `auth`' proxy.log; grep -m1 -o "pass_through_endpoints entry '/echo' sets no.*" proxy.log
  2. Observed

    0
    (no line on either instance)
    

After (31bbe05)

plain key: POST /v1/chat/completions (real provider call)

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' -H 'Authorization: Bearer <key-plain>' http://127.0.0.1:36384/v1/chat/completions -H 'Content-Type: application/json' -d '{"model":"gpt-5-mini","messages":[{"role":"user","content":"say hi"}],"max_completion_tokens":16}'
  2. Observed

    {"id":"chatcmpl-ESAb3wqkZjzHshfz0VmYr3CS9jEhG","created":1790382625,"model":"gpt-5-mini","object":"chat.completion","choices":[{"finish_reason":"length","index":0,"message":{"content":"","role":"assistant","provider_specific_fields":{"refusal":null},"annotations":[]},"provider_specific_fields":{}}],"usage":{"completion_tokens":16,"prompt_tokens":8,"total_tokens":24,"completion_tokens_details":{"accepted_prediction_tokens":0,"audio_tokens":0,"reasoning_tokens":16,"rejected_prediction_tokens":0},"prompt_tokens_details":{"audio_tokens":0,"cached_tokens":0}},"service_tier":"default"}
    HTTP 200
    

no key: GET /echo (auth omitted)

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' http://127.0.0.1:57122/echo
  2. Observed

    {"error":{"message":"Authentication Error, No api key passed in.","type":"auth_error","param":"None","code":"401"}}
    HTTP 401
    

plain key: GET /echo (auth omitted)

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' -H 'Authorization: Bearer <key-plain>' http://127.0.0.1:36384/echo
  2. Observed

    {"args":{},"headers":{"host":"postman-echo.com","accept-encoding":"gzip, br","accept":"*/*","x-lit8631":"repro-omitted","user-agent":"litellm/1.104.0","x-forwarded-proto":"https"},"url":"https://postman-echo.com/get"}
    HTTP 200
    

team-over-budget key: POST /v1/chat/completions control

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' -H 'Authorization: Bearer <key-team-over-budget>' http://127.0.0.1:57122/v1/chat/completions -H 'Content-Type: application/json' -d '{"model":"gpt-5-mini","messages":[{"role":"user","content":"say hi"}],"max_completion_tokens":16}'
  2. Observed

    {"error":{"message":"Budget has been exceeded! Team=0cceec4d-a1cf-4fd1-bea5-57d9e4c9c9d1 Current cost: 3.4e-05, Max budget: 1e-06","type":"budget_exceeded","param":null,"code":"422"}}
    HTTP 422
    

team-over-budget key: GET /echo (auth omitted)

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' -H 'Authorization: Bearer <key-team-over-budget>' http://127.0.0.1:36384/echo
  2. Observed

    {"error":{"message":"Budget has been exceeded! Team=0cceec4d-a1cf-4fd1-bea5-57d9e4c9c9d1 Current cost: 3.4e-05, Max budget: 1e-06","type":"budget_exceeded","param":null,"code":"422"}}
    HTTP 422
    

team-over-budget key: GET /echo (auth omitted), other instance

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' -H 'Authorization: Bearer <key-team-over-budget>' http://127.0.0.1:57122/echo
  2. Observed

    {"error":{"message":"Budget has been exceeded! Team=0cceec4d-a1cf-4fd1-bea5-57d9e4c9c9d1 Current cost: 3.4e-05, Max budget: 1e-06","type":"budget_exceeded","param":null,"code":"422"}}
    HTTP 422
    

team-over-budget key, granted the route: GET /echo-auth-true (auth: true)

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' -H 'Authorization: Bearer <key-team-over-budget>' http://127.0.0.1:36384/echo-auth-true
  2. Observed

    {"error":{"message":"Budget has been exceeded! Team=0cceec4d-a1cf-4fd1-bea5-57d9e4c9c9d1 Current cost: 3.4e-05, Max budget: 1e-06","type":"budget_exceeded","param":null,"code":"422"}}
    HTTP 422
    

no key: GET /echo-auth-false (auth: false)

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' http://127.0.0.1:57122/echo-auth-false
  2. Observed

    {"args":{},"headers":{"host":"postman-echo.com","accept-encoding":"gzip, br","accept":"*/*","x-lit8631":"repro-auth-false","user-agent":"litellm/1.104.0","x-forwarded-proto":"https"},"url":"https://postman-echo.com/get"}
    HTTP 200
    

llm_api_routes-only key: GET /echo (auth omitted)

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' -H 'Authorization: Bearer <key-llm-api-routes>' http://127.0.0.1:36384/echo
  2. Observed

    {"args":{},"headers":{"host":"postman-echo.com","accept-encoding":"gzip, br","accept":"*/*","x-lit8631":"repro-omitted","user-agent":"litellm/1.104.0","x-forwarded-proto":"https"},"url":"https://postman-echo.com/get"}
    HTTP 200
    

llm_api_routes-only key: GET /echo-auth-true (auth: true)

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' -H 'Authorization: Bearer <key-llm-api-routes>' http://127.0.0.1:57122/echo-auth-true
  2. Observed

    {"detail":"Key/team not allowed to access passthrough route /echo-auth-true. Configure `allowed_passthrough_routes` on the team or key."}
    HTTP 403
    

plain key, no grant: GET /echo-auth-true (auth: true)

  1. Run

    curl -s -w '\nHTTP %{http_code}\n' -H 'Authorization: Bearer <key-plain>' http://127.0.0.1:36384/echo-auth-true
  2. Observed

    {"error":{"message":"Key/team not allowed to access passthrough route /echo-auth-true. Configure `allowed_passthrough_routes` on the team or key.","type":"auth_error","param":"None","code":"403"}}
    HTTP 403
    

boot log: warning for the entry that omits auth

  1. Run, on each instance's boot log

    grep -c 'sets no `auth`' proxy.log; grep -m1 -o "pass_through_endpoints entry '/echo' sets no.*" proxy.log
  2. Observed

    3
    pass_through_endpoints entry '/echo' sets no `auth`: any valid LiteLLM key may call it. Set `auth: true` to restrict it to keys granted `allowed_passthrough_routes`, or `auth: false` to serve it without a key. A future release will treat a missing `auth` as `auth: true`.
    (3 lines on each instance: the CLI pre-load plus its two workers)
    
  • auth: true still refuses keys without a route grant, unchanged
  • Grant-denied 403 body differs by key shape, pre-existing, left alone
  • Boot warning prints three times per 2-worker instance, this PR
  • Docs still label auth Enterprise, separate docs PR follows

Type

🐛 Bug Fix

Caveats (if any)

Medium

  • Subpaths of an omitted-auth include_subpath entry now take any valid key
    • Before, they were admin-only without a DB and grant-gated with one
    • Keys scoped to allowed_routes: ["openai_routes"] and team default allowlists reach them now
  • auth: 1, "true", "yes", or "on" was public and now needs a key plus a grant
  • Only the team-budget check ran live; the other checks share its code path
  • Admin UI pass-through page should list config entries read-only now, not re-checked

Low

  • The boot warning repeats per worker process plus the CLI pre-load
  • Flipping the default to auth: true is follow-up work, the warning names it
  • auth: false is now the only way to skip the checks, and it drops the key too
  • The listing still shows auth: true for an omitted-auth config entry, pre-existing, folds into the default flip
  • Config entries in the listing carry a per-worker random id no lookup or delete accepts, pre-existing
  • Omitted-auth paths now count as LLM routes for DISABLE_LLM_API_ENDPOINTS, OAuth2 routing, and the OpenAPI filter, not driven live
  • A config entry with no target lands in the LLM route list though no route registers
  • Docs PR litellm-docs#1739 lands right after this one
  • CircleCI local_testing_part1 at the tip fails only test_get_model_info_bedrock_cross_region_capability_parity, red on main too (pipelines 90441 and 90403), untouched here
  • CircleCI using_litellm_on_windows at the tip timed out on Install Dependencies with zero tests, as on main pipelines 90442 and 90403
  • CircleCI langfuse_logging_unit_tests at the tip timed out in Run tests with zero tests, as on main pipelines 90441 and 90403
  • CircleCI unit at the tip has 35 reds, 31 of them the same tests main pipeline 90441 fails, three test_mcp_client_unit tests that pass locally at the tip and fail on main 90403 too, and test_the_repo_as_it_stands_has_every_shard_child_assigned, which fails at the merge base and is fixed on main by test(zerobus): move tests into active CI selection #43235, so it clears on merge
  • CircleCI integration-accounting at the tip first failed test_rotated_keys_users_and_model_groups_preserve_success_failure_cache_ledger with the same owned-HTTP-peer GET-instead-of-POST assertion main pipeline 90389 hit, and passed on the rerun from failed
  • CircleCI proxy_store_model_in_db_tests at the tip fails only test_e2e_langfuse_callbacks_in_db (request not found in Langfuse traces), the same single red as main pipelines 90441, 90403, and 90394
  • CircleCI build_and_test at the tip fails only the two rust-python-harness namespace import cases (ocr sdk parity modules), the same pair main pipelines 90441, 90403, and 90394 fail
  • CircleCI integration-extensions at the tip hit the 60 minute step cap at 86% and reported no junit, the same cap main pipelines 90441, 90403, 90394, and 90381 hit; the first run's log shows every pass-through visibility and chaos test PASSED and only test_straiker_v3_platform.py::test_burst_survives_one_worker_kill FAILED (a /v1/chat/completions worker-kill burst, nothing pass-through), and the rerun from failed hit the same cap with the straiker test PASSED and only test_passthrough_worker_sigkill_leaves_sibling_serving_and_logging FAILED, which main 90441 fails and 90403 passes and which drives the built-in /gemini provider route this diff does not touch

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR
  • 31bbe05 passes /live-pr-risk

@greptile-apps

greptile-apps Bot commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[Critical risk] Changes how pass-through endpoints enforce authentication.

The PR appears safe to merge based on the current review.

Summary

This PR runs policy checks for config pass-through entries that omit auth, preserves explicit public and route-granted modes, aligns auth-value parsing, avoids registering config-owned entries twice, and adds configuration warnings and tests.

Reviews (2) · Last reviewed commit: "fix(proxy): warn once per pass-through e..."

Comment thread litellm/proxy/pass_through_endpoints/common_utils.py Outdated
Comment thread litellm/proxy/pass_through_endpoints/common_utils.py Outdated
@codecov

codecov Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.95918% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/proxy/auth/user_api_key_auth.py 80.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@codspeed

codspeed Bot commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_passthrough_omitted_auth_policy_checks (31bbe05) with main (a8fe84b)

Open in CodSpeed

@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 31bbe05. Configure here.

This branch was successfully deployed

1 active deployment
e2e-changed — 31bbe05a Deployed Sep 26, 2026 by mateo-berri via oauth #1255
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants