feat(vault): add separate login and secret namespaces for HashiCorp Vault - #41539
Conversation
…ault Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
|
|
|
bugbot run |
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
bugbot run |
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
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 8691a1e. Configure here.
|
The documentation and code-quality failures only need BerriAI/litellm-docs#1511 merged first, since CI reads litellm-docs main. No code change needed here |
|
@veria-ai please review 8691a1e: two new Vault namespace settings for login header and secret URL, legacy fallback, URL keyed secret cache and its invalidation, connection test headers, Admin UI fields |
|
bugbot run |
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
|
@veria-ai please review bc636bc: main merged in, same Vault login and secret namespace split, URL keyed cache, connection test headers, Admin UI fields |
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
bugbot run |
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
|
@veria-ai please review efd2b8a: main merged in, and the KV read body walk is now typed as Mapping[str, object] with isinstance narrowing |
TLDR
Problem this solves:
HCP_VAULT_NAMESPACEdrives both Vault login and secret pathsrootbut stores secrets in a team namespaceHow it solves it:
HCP_VAULT_LOGIN_NAMESPACE:X-Vault-Namespaceheader on AppRole and TLS login onlyHCP_VAULT_SECRET_NAMESPACE: URL path segment on reads, writes, rotates, deletesHCP_VAULT_NAMESPACE, so existing deployments are unchangedsecret_manager_settings.namespacestill wins over the secret namespaceUser Flow
Before: the proxy admin cannot make Vault login and Vault secret paths use different namespaces, so a team-namespaced secret store never works
HCP_VAULT_NAMESPACEto the team namespace, sayteams/team-a, and start the proxy with AppRole credentials that live inrootX-Vault-Namespace: teams/team-aand the role does not exist thereHCP_VAULT_NAMESPACE=root(or leave it unset) so login succeeds{"key_alias": "team-a", "models": ["gpt-5.5"]}and get ask-...key back, but the proxy log shows the Vault write to/v1/secret/data/litellm/team-areturning 403 because the key store is underteams/team-aAuthenticationError: OpenAIException - The api_key client option must be set, because the provider secret is read from/v1/secret/data/OPENAI_API_KEYat root, where it does not existAfter: the same admin logs in at root and keeps every secret under the team namespace
HCP_VAULT_LOGIN_NAMESPACE=rootandHCP_VAULT_SECRET_NAMESPACE=teams/team-a(or type the same two values into the Hashicorp Vault card on the Admin Access page of the Admin UI) and start the proxy with the same AppRole credentialsX-Vault-Namespace: root{"key_alias": "team-a", "models": ["gpt-5.5"]}and get ask-...key back, and Vault now holds it atteams/team-a/secret/data/litellm/team-a/v1/teams/team-a/secret/data/OPENAI_API_KEYRelevant issues
Supersedes #21759, which was closed as stale before
_build_secret_targetand per-secret overrides landedAffected release
Linear ticket
Resolves LIT-1983
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
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 moredocumentationwaits on docs(vault): document separate login and secret namespaces litellm-docs#1511@greptileaito re-request a review after pushing changes)Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
Vault namespaces are an Enterprise feature, and the only Vault available locally is the OSS
hashicorp/vault:1.20image, so namespace enforcement itself was not exercised. What the run below proves is the wire artifact: which namespace the proxy puts on the login request header and which namespace it puts in every secret URL. To make the two arms observably different on OSS Vault, the KV store was laid out the way a namespaced deployment looks from the URL's point of view: on Enterprise a secret in namespaceteams/team-ais addressed as/v1/teams/team-a/secret/data/<name>, so the fixture mounts a KV v2 engine atteams/team-a/secretand scopes the AppRole policy to it, while the defaultsecret/mount stays empty and unreadable by that roleShared setup, identical for both arms
docker run -d --name vault-lit1983 -p 127.0.0.1:8283:8200 --cap-add=IPC_LOCK hashicorp/vault:1.20(dev mode), KV v2 mounts atsecret/andteams/team-a/secret/, policyteam-a-secrets=path "teams/team-a/secret/*" { capabilities = ["create","read","update","delete","list"] }, AppRolelitellmbound to that policy,OPENAI_API_KEYwritten toteams/team-a/secret/data/OPENAI_API_KEY127.0.0.1:8284forwards to Vault and appends one line per request (METHOD path -> status headers=...) withX-Vault-Tokenreplaced by<redacted>; it never logs bodies. The log is truncated before each armlit1983_proxy_config.yaml): oneopenai/gpt-5.5deployment withapi_key: os.environ/OPENAI_API_KEY,key_management_system: hashicorp_vault,store_virtual_keys: true,prefix_for_stored_virtual_keys: "litellm/",hosted_keys: [OPENAI_API_KEY],master_key: sk-1234HCP_VAULT_ADDR=http://127.0.0.1:8284,HCP_VAULT_LOGIN_NAMESPACE=root,HCP_VAULT_SECRET_NAMESPACE=teams/team-a,HCP_VAULT_MOUNT_NAME=secret,HCP_VAULT_APPROLE_ROLE_IDandHCP_VAULT_APPROLE_SECRET_IDfrom the fixture;OPENAI_API_KEYandHCP_VAULT_NAMESPACEunsetPYTHONPATH="$TREE:$TREE/enterprise" python litellm/proxy/proxy_cli.py --config /home/ubuntu/lit1983/lit1983_proxy_config.yaml --port <port> --detailed_debugfrom its own checkout, withlitellm.__file__printed first to prove which tree served the requestsBefore (8fc9c46, the merge base)
Split namespaces, login at root and secrets under teams/team-a
Confirm the tree
Generate a virtual key (the proxy still answers because the DB write succeeds; the Vault copy fails, see step 4)
Call the model with that key: the provider secret cannot be read, so the call fails
What the proxy actually sent to Vault: the two new variables are ignored, the login has no namespace header and every secret path is at root, so Vault denies them
Admin UI, Admin Access page, Hashicorp Vault card
Start the proxy from the base tree on port 4000 (
ui_proxy_config.yaml, same model list, no Vault) and the dashboard from the base tree withnpm run devon port 3000, sign in as the proxy admin withsk-1234Open http://localhost:3000/admin-panel/, expand the Hashicorp Vault card and click Edit Configuration; the Edit Hashicorp Vault Configuration modal opens
The modal shows a single Namespace field; there is nowhere to give login and secrets different namespaces
Screenshot: Hashicorp Vault modal on the merge base, one Namespace field
After (efd2b8a, the PR tip)
Split namespaces, login at root and secrets under teams/team-a
Confirm the tree
Generate a virtual key
Call the model with that key: real OpenAI call, real tokens billed
What the proxy actually sent to Vault: login carries
X-Vault-Namespace: root, the provider secret read and the virtual key write both addressteams/team-ain the URL, and no secret request carries a namespace headerAdmin UI, Admin Access page, Hashicorp Vault card
Start the proxy from the PR tree on port 4000 (
ui_proxy_config.yaml, same model list, no Vault) and the dashboard from the PR tree withnpm run devon port 3000, sign in as the proxy admin withsk-1234Open http://localhost:3000/admin-panel/, expand the Hashicorp Vault card and click Edit Configuration; the Edit Hashicorp Vault Configuration modal opens
The modal now shows Login Namespace and Secret Namespace next to the existing Namespace field
Screenshot: Hashicorp Vault modal on the PR tip, Login Namespace and Secret Namespace fields empty
Fill Namespace
root, Login Namespaceroot, Secret Namespaceteams/team-a; the vitest inEditHashicorpVaultModal.test.tsxasserts these same three fields reach the save payload asvault_namespace,vault_login_namespaceandvault_secret_namespaceScreenshot: Hashicorp Vault modal on the PR tip with the three namespace fields filled
Backward compatibility, legacy config at base and tip
Same fixture, but the proxy environment carries only the legacy variable,
HCP_VAULT_NAMESPACE=teams/team-a, withHCP_VAULT_LOGIN_NAMESPACEandHCP_VAULT_SECRET_NAMESPACEunset. This is the path every existing deployment is on. Both trees log in withX-Vault-Namespace: teams/team-a, read and write underteams/team-ain the URL, and the model call returnsLIT-1983with 34 tokens billedMerge base, 8fc9c46, served from
/home/ubuntu/repos/litellm_base, key aliaslit1983-legacybasePR tip, efd2b8a, served from
/home/ubuntu/repos/litellm, key aliaslit1983-legacytip4Type
🆕 New Feature
Caveats (if any)
Medium
secret_manager_settings(namespace,mount,path_prefix,data) the same way writes already did; before, reads ignored them and always used the global defaultsdatafield is picked at read time. The same secret name in two namespaces no longer collides, two fields of one secret share one entry, and rotate and delete evict that URL entry so no field of the mutated path can be served staledocumentationcheck (andcode-quality) fails withEnvironment variables read under ./litellm but mentioned nowhere in the docs: ['HCP_VAULT_LOGIN_NAMESPACE', 'HCP_VAULT_SECRET_NAMESPACE']until docs(vault): document separate login and secret namespaces litellm-docs#1511 merges, because both workflows check out litellm-docs at its default branch; the same test passes locally against that docs branchLow
vault_login_namespaceandvault_secret_namespaceare stored in the existinghashicorp_secret_managerconfig override row, no new DB columnX-Vault-Namespacewas never sent on secret reads or writes on main (only the URL segment), so this PR keeps that single mechanism rather than introducing a second oneNotes for reviewers
Root cause in one sentence:
HCP_VAULT_NAMESPACEwas the only namespace input and it fed both the login header and the secret URL, so a role that lives in one namespace could never manage secrets in anotherSecurity verdict: this is ordinary feature work, not a vulnerability fix. It only changes which namespace already-held Vault credentials authenticate against and read from; it grants nobody access they did not already have, and Vault still enforces the AppRole policy on every request. No token or secret value is logged anywhere in the change or the proof
Taxonomy audit of the diff (items with a concrete changed line, fixed or judged not applicable). A1/A2 fixed: the cache is keyed by the resolved URL and holds the full response body, so a different namespace cannot collide, a different
dataselector reads its own field from the cached body, and rotate or delete evicts every field of the path at once; one test reads two fields of one path and asserts each comes back, another caches a field, deletes the secret and asserts the next read refetches. W1 fixed: the connection test used to mutate the mapping returned by_get_request_headers; it now builds a separateMappingProxyTypeand a test asserts the original mapping is untouched. X1-X5 checked: the fallbacks useis not Noneso an explicitly empty login or secret namespace is honoured as "no namespace" instead of silently falling back toHCP_VAULT_NAMESPACE;_get_login_headerstreats empty as "omit the header", which is how Vault addresses root. C1-C7 checked: no default flips, precedence for existing deployments is unchanged because both new values fall back to the legacy one. F3 covered: sync and async reads, write, rotate, delete and both login methods all route through the same two properties; the connection test endpoint was the sibling surface and is included. T1 covered: every namespace test intest_hashicorp_secret_manager.pyfails on main (the login header and URL assertions differ), and the tests assert the artifacts the HTTP client received viarespx, not the helper's return value. H3/H4: no new source comments,X | None, no baredictorAnyin new code (_get_secret_value_from_json_responsetakesMapping[str, object] | Noneand narrows each nested level through_as_json_object),schema.d.tsand the lazy OpenAPI snapshot regenerated vianpm run gen:apiandpython -m litellm.proxy._lazy_openapi_snapshot. M2 not applicable: no new secret is logged or returned. R not applicable: no migration, the config override table already stores the whole Vault block as JSONFinal Attestation
os.environ/model params, the virtual key write, rotate and delete hooks, the Vault connection test endpoint and the Admin UI form; reads, the key write and both login shapes (split namespaces, legacy single namespace) were driven live at base and tip with the same fixture and real provider calls, the Admin UI modal was opened on both trees, rotate and delete are covered by the respx tests that assert the URL each one hitsLink to Devin session: https://app.devin.ai/sessions/f6aabc2074cc4c5d9ccefa90a49d3636
Open in Devin Desktop: https://app.devin.ai/desktop/session/f6aabc2074cc4c5d9ccefa90a49d3636?variant=devin
Requested by: @yassin-berriai
Link to Devin session: https://app.devin.ai/sessions/798e6e6460fc40deb35c1219944662cf
Open in Devin Desktop: https://app.devin.ai/desktop/session/798e6e6460fc40deb35c1219944662cf?variant=devin