Repository navigation
fix(proxy): restore pre-config-wins handling of pass-through endpoints - #43962
Conversation
Config-wins (#41779) made general_settings.pass_through_endpoints a config-owned key. The DB reader then got the config list back as if it were DB rows, re-registered each entry without forward_headers on every DB sync, and the stripped copy won the route lookup, so a config pass-through with forward_headers: true stopped forwarding Authorization. UI create, update and delete of pass-throughs were also rejected while the config declared any. This puts pass-throughs back on their pre-#41779 path: the settings store no longer lets the config own the key, the config list is captured env-resolved at load_config, each DB sync merges DB entries with config entries on paths the DB does not declare, and /config/field/info reads the stored rows only. A UI pass-through write re-applies that merge immediately so the config entries stay served until the next sync.
…ng-auth-false-ed0a9a
|
| self.settings["pass_through_endpoints"] = _SETTINGS_LIST.validate_python( | ||
| (*db_endpoints, *config_endpoints_beside_db) | ||
| ) | ||
| await initialize_pass_through_endpoints(pass_through_endpoints=_ENDPOINT_DICTS.validate_python(db_endpoints)) |
There was a problem hiding this comment.
High: Pass-through overrides authorize the wrong upstream
For a YAML POST /shared targeting a private upstream (auth: true) and a DB replacement POST /shared targeting a public upstream (auth: false), the merged auth list permits anonymous requests, but initialize_pass_through_endpoints() still registers both entries and the first-match registry selects the YAML entry registered at startup. An unauthenticated attacker can now reach the private upstream with its configured credentials; apply the same DB-wins filtering to route registration and remove shadowed YAML registry entries before publishing the replacement authentication policy.
There was a problem hiding this comment.
Pre-existing: pre-#41779 and v1.102 behave the same way. This PR reverts to that handling to fix a regression, so fixing route registration is a follow-up
PR overviewThe PR restores pre-configuration precedence handling for pass-through endpoints in the proxy, updating how YAML and database endpoint definitions are combined. One issue has been addressed, but an authorization mismatch remains between merged endpoint settings and route registration. When a public database endpoint overrides a private YAML endpoint at the same path, anonymous requests can still be routed to the private upstream using its configured credentials. This leaves a concrete authentication bypass open until registration follows the same override rules and removes shadowed entries. Open issues (1)
Fixed/addressed: 1 · PR risk: 8/10 |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
get_config now returns DB pass-throughs plus config ones on other paths, each DB sync republishes that merged list, and /config/field/info reads pass_through_endpoints from the DB row so a UI write never drops stored entries when models are not stored in the DB
|
@greptileai please re-review the latest commit |
load_yaml cleared the runtime pass-through list, so auth: false routes answered 401 while get_config awaited the database
A lagging read replica could return an older list, and the UI create and edit flows write the whole field back
|
@greptileai please re-review the latest commits |
|
@greptileai please re-review |
The kept runtime list was merged as if it were DB entries, so an edited config entry on the same path was dropped. Merge the stored DB row with the fresh config instead, and give the field-info test mock a writer
|
@greptileai please re-review the latest commit |
|
bugbot run |
get_config resets the stored DB rows before reading them again, which cleared the served pass-through list and made auth: false routes answer 401 for the length of the read
|
@greptileai please re-review the latest commit |
|
bugbot run |
There was a problem hiding this comment.
✅ 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 e5ef842. Configure here.
basedpyright rejects a Final variable assigned inside a loop
…e_1_103_x fix(proxy): backport #43962 to stable/1.103.x
…ject_key_prefix * upstream/main: (62 commits) fix(guardrails): scan Responses API input in Azure Prompt Shield (BerriAI#43786) feat(lens): investigate sampled traces and retain batch results (BerriAI#43942) fix(proxy): restore pre-config-wins handling of pass-through endpoints (BerriAI#43962) fix(cost-map): raise baseten DeepSeek-V4.1-Flash max output to 262144 (BerriAI#43916) chore(cost-map): add deprecation date for anthropic claude-sonnet-4-5 (BerriAI#43898) chore(cost-map): add fireworks inkling priority prices from the prices api (BerriAI#43949) feat(guardrails): honor litellm_params.timeout in every HTTP guardrail (BerriAI#43134) test(e2e): typed per-test metadata for the e2e suite (BerriAI#42044) fix(caching): write the response-cache SET to Redis at once instead of on the post-call batch (BerriAI#43973) feat(ui): filter tags by name and description on the Tag Management page (BerriAI#42949) feat(providers): add Cortecs as an OpenAI-compatible provider (BerriAI#43872) feat(e2e): record each e2e test's steps, starting with ProxyClient (BerriAI#42393) test(ci): repair stale tests and move retired OpenAI text-completion fixtures (BerriAI#43958) feat(proxy): record in spend logs whether a request used a client-forwarded Anthropic OAuth token (BerriAI#43063) fix(azure_storage): keep the DataLakeServiceClient alive until its TTL elapses (BerriAI#43082) chore(deps): bump gitpython and tornado, extend diskcache osv ignore to Nov 1 (BerriAI#43961) fix(guardrails): treat an unknown straiker api_version as unset instead of skipping the guardrail (BerriAI#43956) fix(azure_storage): name Data Lake objects without base64 padding or slashes (BerriAI#43914) fix(grayswan): send request conversation and tool calls to post-call monitor (BerriAI#43770) chore(cost-map): sync openrouter prices from the models API (BerriAI#43950) ...
BerriAI#43962) * fix(proxy): restore pre-config-wins handling of pass-through endpoints Config-wins (BerriAI#41779) made general_settings.pass_through_endpoints a config-owned key. The DB reader then got the config list back as if it were DB rows, re-registered each entry without forward_headers on every DB sync, and the stripped copy won the route lookup, so a config pass-through with forward_headers: true stopped forwarding Authorization. UI create, update and delete of pass-throughs were also rejected while the config declared any. This puts pass-throughs back on their pre-BerriAI#41779 path: the settings store no longer lets the config own the key, the config list is captured env-resolved at load_config, each DB sync merges DB entries with config entries on paths the DB does not declare, and /config/field/info reads the stored rows only. A UI pass-through write re-applies that merge immediately so the config entries stay served until the next sync. * fix(proxy): keep config pass-throughs in every reload of the merged list get_config now returns DB pass-throughs plus config ones on other paths, each DB sync republishes that merged list, and /config/field/info reads pass_through_endpoints from the DB row so a UI write never drops stored entries when models are not stored in the DB * fix(proxy): keep serving pass-throughs while the config file reloads load_yaml cleared the runtime pass-through list, so auth: false routes answered 401 while get_config awaited the database * fix(proxy): read stored pass-throughs from the writer before a UI write A lagging read replica could return an older list, and the UI create and edit flows write the whole field back * fix(proxy): apply config file pass-through auth changes on reload The kept runtime list was merged as if it were DB entries, so an edited config entry on the same path was dropped. Merge the stored DB row with the fresh config instead, and give the field-info test mock a writer * fix(proxy): keep pass-throughs served while a DB sync reads the database get_config resets the stored DB rows before reading them again, which cleared the served pass-through list and made auth: false routes answer 401 for the length of the read * refactor(proxy): move the settings store reload out of the loop basedpyright rejects a Final variable assigned inside a loop (cherry picked from commit 2eb2bf1)
#43962) (#44054) * fix(proxy): restore pre-config-wins handling of pass-through endpoints Config-wins (#41779) made general_settings.pass_through_endpoints a config-owned key. The DB reader then got the config list back as if it were DB rows, re-registered each entry without forward_headers on every DB sync, and the stripped copy won the route lookup, so a config pass-through with forward_headers: true stopped forwarding Authorization. UI create, update and delete of pass-throughs were also rejected while the config declared any. This puts pass-throughs back on their pre-#41779 path: the settings store no longer lets the config own the key, the config list is captured env-resolved at load_config, each DB sync merges DB entries with config entries on paths the DB does not declare, and /config/field/info reads the stored rows only. A UI pass-through write re-applies that merge immediately so the config entries stay served until the next sync. * fix(proxy): keep config pass-throughs in every reload of the merged list get_config now returns DB pass-throughs plus config ones on other paths, each DB sync republishes that merged list, and /config/field/info reads pass_through_endpoints from the DB row so a UI write never drops stored entries when models are not stored in the DB * fix(proxy): keep serving pass-throughs while the config file reloads load_yaml cleared the runtime pass-through list, so auth: false routes answered 401 while get_config awaited the database * fix(proxy): read stored pass-throughs from the writer before a UI write A lagging read replica could return an older list, and the UI create and edit flows write the whole field back * fix(proxy): apply config file pass-through auth changes on reload The kept runtime list was merged as if it were DB entries, so an edited config entry on the same path was dropped. Merge the stored DB row with the fresh config instead, and give the field-info test mock a writer * fix(proxy): keep pass-throughs served while a DB sync reads the database get_config resets the stored DB rows before reading them again, which cleared the served pass-through list and made auth: false routes answer 401 for the length of the read * refactor(proxy): move the settings store reload out of the loop basedpyright rejects a Final variable assigned inside a loop (cherry picked from commit 2eb2bf1)
…ws on the hub Main merges database pass-throughs with config file ones on other paths since #43962, so a database write beside a config entry is accepted rather than rejected as config-owned Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…03.2) (#328) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ghcr.io/berriai/litellm](https://images.chainguard.dev/directory/image/wolfi-base/overview) ([source](https://github.com/BerriAI/litellm)) | patch | `v1.103.1` → `v1.103.2` | --- ### Release Notes <details> <summary>BerriAI/litellm (ghcr.io/berriai/litellm)</summary> ### [`v1.103.2`](https://github.com/BerriAI/litellm/releases/tag/v1.103.2) [Compare Source](BerriAI/litellm@v1.103.1...v1.103.2) #### Verify Docker Image Signature All LiteLLM Docker images are signed with [cosign](https://docs.sigstore.dev/cosign/overview/). Every release is signed with the same key introduced in [commit `0112e53`](BerriAI/litellm@0112e53). **Verify using the pinned commit hash (recommended):** A commit hash is cryptographically immutable, so this is the strongest way to ensure you are using the original signing key: ```bash cosign verify \ --key https://raw.githubusercontent.com/BerriAI/litellm/0112e53046018d726492c814b3644b7d376029d0/cosign.pub \ ghcr.io/berriai/litellm:v1.103.2 ``` **Verify using the release tag (convenience):** Tags are protected in this repository and resolve to the same key. This option is easier to read but relies on tag protection rules: ```bash cosign verify \ --key https://raw.githubusercontent.com/BerriAI/litellm/v1.103.2/cosign.pub \ ghcr.io/berriai/litellm:v1.103.2 ``` Expected output: ``` The following checks were performed on each of these signatures: - The cosign claims were validated - The signatures were verified against the specified public key ``` *** #### What's Changed - chore(release): sync stable/1.103.x to v1.103.1 by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​43824](BerriAI/litellm#43824) - fix(proxy): backport [#​40541](BerriAI/litellm#40541), [#​43642](BerriAI/litellm#43642), and [#​43656](BerriAI/litellm#43656) to stable/1.103.x for v1.103.2 by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​43897](BerriAI/litellm#43897) - fix(anthropic): backport [#​42152](BerriAI/litellm#42152) and [#​42288](BerriAI/litellm#42288) to stable/1.103.x by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​43662](BerriAI/litellm#43662) - fix(proxy): backport [#​43962](BerriAI/litellm#43962) to stable/1.103.x by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​43984](BerriAI/litellm#43984) **Full Changelog**: <BerriAI/litellm@v1.103.1...v1.103.2> </details> --- ### Configuration 📅 **Schedule**: (in timezone America/New_York) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate CLI](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMTUuMTMiLCJ1cGRhdGVkSW5WZXIiOiI0NC4xMTUuMTMiLCJ0YXJnZXRCcmFuY2giOiJtYWluIiwibGFiZWxzIjpbInJlbm92YXRlL2NvbnRhaW5lciIsInR5cGUvcGF0Y2giXX0=--> Reviewed-on: https://git.greyrock.io/todd/home-ops/pulls/328
…th (#44267) The failure spend-log row from #42695 is written for pass-through routes that run as LLM API routes, which a config route only does with auth: true. The test omitted auth and passed only while config wins (#41779) registered config entries through the typed model, where auth defaults to true. #43962 restored the pre-config-wins registration, so the route lost that status and the row was never written. Set auth: true on the route so the test covers the logging it was written for without depending on that side effect (cherry picked from commit 1d9cd9b)
…th (#44269) The failure spend-log row from #42695 is written for pass-through routes that run as LLM API routes, which a config route only does with auth: true. The test omitted auth and passed only while config wins (#41779) registered config entries through the typed model, where auth defaults to true. #43962 restored the pre-config-wins registration, so the route lost that status and the row was never written. Set auth: true on the route so the test covers the logging it was written for without depending on that side effect (cherry picked from commit 1d9cd9b)
…th (#44265) The failure spend-log row from #42695 is written for pass-through routes that run as LLM API routes, which a config route only does with auth: true. The test omitted auth and passed only while config wins (#41779) registered config entries through the typed model, where auth defaults to true. #43962 restored the pre-config-wins registration, so the route lost that status and the row was never written. Set auth: true on the route so the test covers the logging it was written for without depending on that side effect
TLDR
Problem this solves:
forward_headers: truestopped forwardingAuthorizationSTORE_MODEL_IN_DB=true, starting in v1.103.0os.environ/pass-through targets were sent upstream unresolved and returned 500How it solves it:
general_settings.pass_through_endpoints, so the DB reader sees DB rows onlyUser Flow
Before: a team whose backend validates the caller's JWT gets "token not found" for every request through a config pass-through
forward_headers: trueandauth: falsein the config, withSTORE_MODEL_IN_DB=trueAuthorization: Bearer <jwt>Authorizationheader and rejects itAfter: the same request reaches the backend with the caller's JWT, and UI-created pass-throughs work next to the config ones
Authorization: Bearer <jwt>Authorization: Bearer <jwt>unchanged, right after boot and after every DB syncAffected release
Regression in v1.103.0-rc.1, still present on v1.103.0 and main
Linear ticket
Resolves LIT-9020
Pre-Submission checklist
Screenshots / Proof of Fix
Live proxy on Postgres with
STORE_MODEL_IN_DB=True,PT_ENV_TARGET=http://127.0.0.1:9081/api/envtarget, and a local echo server on :9081 that returns the path andAuthorizationheader it received. The same scenario script ran against both commits on a fresh database each time. Config:Before (38b0762)
Config pass-through with
forward_headers: trueandauth: falseSend the caller's JWT right after boot and the first DB sync; the echo upstream reports which
Authorizationit receivedSame request after further DB sync cycles
Config pass-through with an
os.environ/targetCall the pass-through whose target is
os.environ/PT_ENV_TARGETUI-created pass-through next to the config ones
Create a pass-through through the Admin UI API
Call it right away
List pass-throughs after a DB sync (summarized as path,
is_from_config,auth)After (b7fbc2f)
Config pass-through with
forward_headers: trueandauth: falseSend the caller's JWT right after boot and the first DB sync; the echo upstream reports which
Authorizationit receivedSame request after further DB sync cycles
Config pass-through with an
os.environ/targetCall the pass-through whose target is
os.environ/PT_ENV_TARGETUI-created pass-through next to the config ones
Create a pass-through through the Admin UI API
Call it right away
List pass-throughs after a DB sync (summarized as path,
is_from_config,auth)The same scenario, extended with UI update and delete, an unrelated
/config/field/updatewrite,/config/field/info, and a restart, matched pre-#41779 main call for call except one: a UI-createdauth: falsepass-through now serves right away instead of answering 401 until the next DB sync. It matched v1.102.2 for every config-only callType
🐛 Bug Fix
Caveats (if any)
Medium
GET /config/pass_through_endpointlists config headers withos.environ/values resolved, same as v1.102Low
auth: falsepass-through now serves immediately; before it answered 401 until the next DB syncenable_jwt_authplusSERVER_ROOT_PATH, the customer's exact auth setupFinal Attestation
Note
Medium Risk
Changes proxy config resolution, pass-through route registration, and auth behavior when YAML and DB declare the same path; regression fix but affects request routing and admin settings APIs.
Overview
pass_through_endpointsis no longer config-owned when declared in YAML. The proxy treats it as a resource list (DB-primary): YAML no longer blocks Admin UI writes, and config reloads keep serving merged endpoint state instead of wiping runtime routes.Serving model: DB-stored endpoints are registered from the database; config-file endpoints on other paths are merged in via
_pass_through_endpoints_beside_db. Resolved config exposed to loaders includes that union. On the same path, the DB entry wins for what gets served (restores pre–#41779 behavior).Lifecycle changes:
_apply_pass_through_settingsonly re-initializes routes when the DB payload includes a list; deletes publish an empty DB slice while keeping config paths. General-settings create/update/delete for this field calls_serve_pass_through_endpointsimmediately.GET /config/field/infouses_declared_general_settingso resource-list fields read from the DB row when appropriate.Tests add broad integration coverage (forward_headers, env targets, UI CRUD, reload/sync) and update expectations where YAML+DB overlap now locks auth from the DB side.
Reviewed by Cursor Bugbot for commit e5ef842. Bugbot is set up for automated code reviews on this repo. Configure here.