Re: [I] bug: ai-proxy `auth.header` re-encrypted on every PATCH, causing 401 from upstream LLM [apisix]

2026-06-11 Thread via GitHub


nic-6443 closed issue #13351: bug: ai-proxy `auth.header` re-encrypted on every 
PATCH, causing 401 from upstream LLM
URL: https://github.com/apache/apisix/issues/13351


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]



Re: [I] bug: ai-proxy `auth.header` re-encrypted on every PATCH, causing 401 from upstream LLM [apisix]

2026-06-05 Thread via GitHub


hanzhenfang commented on issue #13351:
URL: https://github.com/apache/apisix/issues/13351#issuecomment-4630719373

   Hi, @Baoyuantop. I have reproduced the issue underlying Admin API `PATCH` 
re-encryption behavior with a minimal local Docker Compose environment.
   
   ### Environment
   
   - APISIX: `apache/apisix:3.16.0-debian`
   - etcd: `bitnamilegacy/etcd:3.6.4`
   - Deployment mode: traditional mode with etcd
   - Data encryption: enabled by default in this image
   - Upstream services: `nginx:alpine`
   - Admin API key used locally: `my-secret-admin-key`
   
   ### Reproduction summary
   
   I used a route containing two plugins:
   
   - `openid-connect`, which declares encrypted fields such as `client_secret`
   - `response-rewrite`, used only as a sibling plugin to PATCH
   
   After creating the route, the raw etcd value showed that 
`openid-connect.client_secret` was encrypted once.
   
   Then I sent a `PATCH` request that only modified the sibling 
`response-rewrite` plugin. The PATCH payload did not include `openid-connect` 
and did not touch `client_secret`.
   
   However, after the PATCH, the raw etcd value of 
`openid-connect.client_secret` changed and became longer. In my local run the 
encrypted value length changed like this:
   
   1. After initial route creation: length `24`
   2. After the first PATCH to `response-rewrite`: length `44`
   3. After a second PATCH to `response-rewrite`: length `64`
   
   This strongly suggests that the already encrypted `client_secret` is being 
encrypted again on every PATCH, even when the encrypted field is unrelated to 
the PATCH payload.
   
   ### Suspected code path
   
   Based on the local behavior, the likely issue is in the generic Admin API 
PATCH write path rather than in `openid-connect` itself.
   
   1. The PATCH handler reads the existing resource from etcd.
   
  At this point, encrypted plugin fields are still stored as ciphertext in 
the resource value.
   
   2. The PATCH handler applies the requested patch or merge directly onto that 
etcd value.
   
  This means the merged resource now contains a mix of newly patched 
plaintext fields and pre-existing encrypted fields.
   
   3. The merged resource is passed through the normal resource validation path.
   
  During this step, the resource-level encryption hook is called again 
before writing the resource back to etcd.
   
   4. For routes, that encryption hook walks all configured plugins, not only 
the plugin or field included in the PATCH request.
   
  Therefore, encrypted fields in sibling plugins are processed again.
   
   5. The plugin encryption helper encrypts every field listed in 
`encrypt_fields` whenever that field exists.
   
  It does not appear to check whether the value is already ciphertext, and 
the PATCH path does not appear to decrypt the old etcd value before 
re-encrypting the merged result.
   
   So the effective flow looks like:
   
   1. Read old route from etcd with encrypted fields still encrypted
   2. Patch only a sibling plugin
   3. Validate the merged route
   4. Re-run encryption for all plugin `encrypt_fields`
   5. Store the already encrypted field with one more encryption layer
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]



[I] bug: ai-proxy `auth.header` re-encrypted on every PATCH, causing 401 from upstream LLM [apisix]

2026-05-11 Thread via GitHub


janiussyafiq opened a new issue, #13351:
URL: https://github.com/apache/apisix/issues/13351

   ### Current Behavior
   
   When a Route has `ai-proxy` (or `ai-proxy-multi`) plus any other plugin, a 
`PATCH` against the Route — even one that doesn't touch `ai-proxy` — corrupts 
`ai-proxy.auth.header` and subsequent requests to that Route return `401 
Incorrect API key provided` from the upstream LLM.
   
   The Admin GET on the Route still returns the correct plaintext 
`auth.header.Authorization` because `apisix/admin/init.lua` decrypts responses 
(`utils.decrypt_params(plugin.decrypt_conf, data)` for 
non-consumer/non-metadata resources), so the breakage is invisible from the 
Admin API surface but very real on the data plane.
   
   **Root cause.** The Admin PATCH path (`apisix/admin/resource.lua:_M:patch`) 
reads the encrypted node from etcd, merges/patches the request payload into it, 
then unconditionally calls `self:check_conf` → `encrypt_conf` on the merged 
value. `encrypt_conf` (`apisix/plugin.lua:1092`) walks each plugin's 
`encrypt_fields` and re-encrypts with no idempotency / decrypt-first guard. So 
fields that were already ciphertext get wrapped in a second AES-CBC layer.
   
   Because AES-CBC in APISIX uses a *fixed IV* (`apisix/ssl.lua:95`), 
encryption is deterministic — a same-plaintext stability check would normally 
look stable, but the *length* of `auth.header.Authorization` grows on every 
PATCH (≈256 → ≈380 → … base64 chars in my MRE), which is direct proof of nested 
encryption.
   
   At request time the data plane decrypts only once via `plugin_checker` → 
`decrypt_conf`, so the still-ciphertext bearer token is sent verbatim to OpenAI 
/ Anthropic / etc. and rejected.
   
   **Affected versions.**
   - Latent since **3.1.0** — `encrypt_fields` infrastructure landed in PR 
#8487 (commit `152ea80e`).
   - Reachable by default from **3.10.0** — `enable_encrypt_fields: true` 
flipped to default in PR #11076 (commit `cca94f10`).
   - Any plugin declaring `encrypt_fields` is in principle vulnerable on PATCH; 
`ai-proxy` / `ai-proxy-multi` are the user-facing failure mode because the 
leaked secret is the upstream API key in `auth.header` / `auth.query` / 
`auth.aws.*` / `auth.gcp.service_account_json`.
   
   ### Expected Behavior
   
   PATCHing a sibling plugin on a Route must not mutate `ai-proxy.auth.header`. 
Equivalently: every Admin write path must invoke `encrypt_conf` on plaintext 
only. The fix is to decrypt the fields listed in each plugin's `encrypt_fields` 
(recursing into nested schemas like `ai-proxy-multi.instances[*].auth.*`) after 
reading the node from etcd and before merge/patch.
   
   ### Error Logs
   
   Upstream rejection (OpenAI shown; same shape for any provider whose key is 
in `auth.header`):
   
   ```json
   {
 "error": {
   "message": "Incorrect API key provided: 5UhVzbqv***. You can find your 
API key at https://platform.openai.com/account/api-keys.";,
   "type": "invalid_request_error",
   "param": null,
   "code": "invalid_api_key"
 }
   }
   ```
   
   The `5UhVzbqv***` prefix is the base64 of the *first* encryption layer being 
sent to OpenAI as the bearer token — i.e. the data plane only peeled off one 
layer.
   
   ### Steps to Reproduce
   
   Prereqs: APISIX ≥ 3.10.0 with the default `enable_encrypt_fields: true`, 
Admin API on `127.0.0.1:9180` with the default key 
`edd1c9f034335f136f87ad84b625c8f1`, and `OPENAI_API_KEY` exported.
   
   1. Create a Route with `ai-proxy` + a second plugin (here 
`ai-prompt-decorator`):
   
  ```bash
  curl -i http://127.0.0.1:9180/apisix/admin/routes/1 \
-H "X-API-KEY: edd1c9f034335f136f87ad84b625c8f1" -X PUT -d '{
  "uri": "/v1/chat/completions",
  "plugins": {
"ai-proxy": {
  "provider": "openai",
  "auth": { "header": { "Authorization": "Bearer 
'"$OPENAI_API_KEY"'" } },
  "options": { "model": "gpt-4o-mini" }
},
"ai-prompt-decorator": {
  "prepend": [{ "role": "system", "content": "You are helpful." }]
}
  }
}'
  ```
   
   2. Confirm the route works:
   
  ```bash
  curl -sS http://127.0.0.1:9080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hi"}]}'
  ```
   
  → 200 with a normal completion.
   
   3. PATCH only the *non-*`ai-proxy` plugin:
   
  ```bash
  curl -i 
http://127.0.0.1:9180/apisix/admin/routes/1/plugins/ai-prompt-decorator \
-H "X-API-KEY: edd1c9f034335f136f87ad84b625c8f1" -X PATCH -d '{
  "prepend": [{ "role": "system", "content": "You are concise." }]
}'
  ```
   
   4. Repeat the chat call from step 2 → `401 Incorrect API key provided: 
***`.
   
   5. (Optional) Compare the *length* of 
`plugins["ai-proxy"].auth.header.Authorization` in the raw PATCH response body