Found while testing #117 live.
Once a user mints API keys for more than one model, per-model scoping breaks: a key minted for model A is accepted for model B, and the user's other keys stop authenticating entirely.
Root cause: key-manager/internal/secrets/manager.go computes the key sequence by counting the user's keys in that one model's Secret, so the first key a user mints for any model is always user-<name>-1. The operator pools every model's api-keys Secret into each model's SecurityPolicy credentialRefs (the pooled-authentication design in #117) and forwards the matched data key as x-llm-client-id for per-model authorization. Identical client IDs across models collide in that pooled set:
- only one Secret's value survives per duplicated client ID, so the user's other keys return 401
- the surviving client ID is in every model's allow-list, so that key is authorized cross-model (200)
Live evidence: user llm-test with keys for two OpenRouter models and one served model. All three keys had client ID user-llm-test-1. Only one key authenticated, and it returned 200 on a model it was not minted for.
Impact: defeats the per-model scoping #117 implements (its stated guarantee is "a key minted for model A returns 200 for A and 403 for every other model"), and makes legitimately-minted keys unusable. Surfaces whenever one user has keys for multiple models; single-model-per-user tests do not hit it.
Fix: scope the client ID by model (user-<name>-<model>-<n>) so it is globally unique. Done on the #117 branch (commit d9f5a15) with a regression test that mints for two models as one user and asserts distinct client IDs.
Found while testing #117 live.
Once a user mints API keys for more than one model, per-model scoping breaks: a key minted for model A is accepted for model B, and the user's other keys stop authenticating entirely.
Root cause:
key-manager/internal/secrets/manager.gocomputes the key sequence by counting the user's keys in that one model's Secret, so the first key a user mints for any model is alwaysuser-<name>-1. The operator pools every model's api-keys Secret into each model's SecurityPolicycredentialRefs(the pooled-authentication design in #117) and forwards the matched data key asx-llm-client-idfor per-model authorization. Identical client IDs across models collide in that pooled set:Live evidence: user
llm-testwith keys for two OpenRouter models and one served model. All three keys had client IDuser-llm-test-1. Only one key authenticated, and it returned 200 on a model it was not minted for.Impact: defeats the per-model scoping #117 implements (its stated guarantee is "a key minted for model A returns 200 for A and 403 for every other model"), and makes legitimately-minted keys unusable. Surfaces whenever one user has keys for multiple models; single-model-per-user tests do not hit it.
Fix: scope the client ID by model (
user-<name>-<model>-<n>) so it is globally unique. Done on the #117 branch (commit d9f5a15) with a regression test that mints for two models as one user and asserts distinct client IDs.