symptom
switchyard-server 0.2.0 copies inbound non-reserved headers onto the upstream http call. a client x-goog-api-key or any other x-* secret (except the reserved authorization / x-api-key / cookie set) is sent to whatever backend the admin configured.
reproduction
capture rig (no provider keys):
python3 tools/capture_headers.py 9999 cap.jsonl canned-openai.json
switchyard-server --config switchyard-capture.toml --host 127.0.0.1 --port 9000
curl -s localhost:9000/v1/chat/completions \
-H 'content-type: application/json' \
-H 'x-goog-api-key: CANARY_X_GOOG_API_KEY' \
-H 'x-custom-internal-secret: CANARY_X_CUSTOM_SECRET' \
-H 'authorization: Bearer CANARY_AUTHORIZATION' \
-d '{"model":"captured-model","messages":[{"role":"user","content":"ping"}]}'
the mock's recorded request includes x-goog-api-key: CANARY_X_GOOG_API_KEY and x-custom-internal-secret: CANARY_X_CUSTOM_SECRET. authorization on the upstream is the deployment value Bearer x, not the client bearer. same headers also appear when the inbound path is post /v1/messages. 5/5.
live gemini (openai-compat base url, api_key_env = GEMINI_API_KEY): the same fake x-goog-api-key still returns http 200 because google prefers the configured bearer. the mock is what shows the header still went out.
expected vs. actual
- expected: secret-bearing client headers such as
x-goog-api-key are dropped unless the admin opts into header forwarding, matching how authorization / x-api-key / cookie are already reserved.
- actual: those headers are attached to the upstream request by default. litellm only does this when
forward_client_headers_to_llm_api is true (off by default).
environment
- switchyard version (or commit sha): 0.2.0 (commit 2bef154)
- python version (
python --version): n/a (rust switchyard-server)
- os / arch: macos arm64
- install path: source build
tools/switchyard/target/release/switchyard-server
- inbound format (chat completions / anthropic messages / responses): chat completions and anthropic messages
- backend (openai / anthropic / nvidia inference hub / other): openai-compatible mock, plus live gemini 2.5 flash via
https://generativelanguage.googleapis.com/v1beta/openai
additional context
this is a confidentiality leak (client secrets leave the proxy), not an auth bypass of the deployment key. no real provider keys appeared in error bodies.
wire captures and replay tests: Atharva-Kanherkar/kairo#3
symptom
switchyard-server 0.2.0 copies inbound non-reserved headers onto the upstream http call. a client
x-goog-api-keyor any otherx-*secret (except the reservedauthorization/x-api-key/cookieset) is sent to whatever backend the admin configured.reproduction
capture rig (no provider keys):
the mock's recorded request includes
x-goog-api-key: CANARY_X_GOOG_API_KEYandx-custom-internal-secret: CANARY_X_CUSTOM_SECRET.authorizationon the upstream is the deployment valueBearer x, not the client bearer. same headers also appear when the inbound path ispost /v1/messages. 5/5.live gemini (openai-compat base url,
api_key_env = GEMINI_API_KEY): the same fakex-goog-api-keystill returns http 200 because google prefers the configured bearer. the mock is what shows the header still went out.expected vs. actual
x-goog-api-keyare dropped unless the admin opts into header forwarding, matching howauthorization/x-api-key/cookieare already reserved.forward_client_headers_to_llm_apiis true (off by default).environment
python --version): n/a (rustswitchyard-server)tools/switchyard/target/release/switchyard-serverhttps://generativelanguage.googleapis.com/v1beta/openaiadditional context
this is a confidentiality leak (client secrets leave the proxy), not an auth bypass of the deployment key. no real provider keys appeared in error bodies.
wire captures and replay tests: Atharva-Kanherkar/kairo#3