Repository navigation
fix(azure): restrict the storage credential chain to deployment identities - #39637
yucheng-berri merged 4 commits into
Conversation
…ities The keyless Azure Storage path walks the full DefaultAzureCredential chain, so a proxy with no storage service principal authenticates as whichever identity the host happens to carry: an operator's az login on a workstation, or the AZURE_CLIENT_ID/AZURE_CLIENT_SECRET service principal set for Azure OpenAI. Neither is the identity granted Storage Blob Data Contributor. Narrow the chain to workload identity and managed identity, the two credentials a deployment legitimately holds. Azure OpenAI, Postgres IAM auth and the other callers of get_azure_ad_token_provider keep the full chain.
There was a problem hiding this comment.
Note
This report is out of date. Scroll down for Devin Review's latest report on this PR.
🔍 Devin Review: 2 flags
Not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)
Greptile SummaryThe PR hardens Azure Storage authentication while leaving other Azure token-provider callers on their existing credential behavior.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| litellm/integrations/azure_storage/azure_storage.py | Selects the deployment-identity credential type for cached, storage-scoped token providers. |
| litellm/secret_managers/get_azure_ad_token_provider.py | Adds explicit construction of a workload-identity and managed-identity credential chain. |
| litellm/types/secret_managers/get_azure_ad_token_provider.py | Extends the Azure credential enum with the deployment-identity option. |
| tests/test_litellm/integrations/azure_storage/test_azure_storage.py | Updates storage credential expectations and replaces mutable credential capture with direct mock-call inspection. |
| tests/test_litellm/secret_managers/test_get_azure_ad_token_provider.py | Covers the deployment credential chain across workload, user-assigned, and system-assigned identity configurations. |
Reviews (4): Last reviewed commit: "fix(azure): let the storage chain reach ..." | Re-trigger Greptile
|
@greptileai please review the current head 0f61ead. The accumulator in both test helpers is gone, replaced by the mock's own call record. |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
bugbot run |
|
@greptileai please review the current head f52771c. It deletes a stray file; the source is unchanged. |
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
1 issue from previous review remains unresolved.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit f52771c. Configure here.
…ntity DefaultAzureCredential keeps one managed identity link and pins it to AZURE_CLIENT_ID, so a host that sets that variable for Azure OpenAI and runs as a system assigned identity never got asked for a storage token. Build the chain from the three credentials a deployment can carry instead of subtracting the ones it cannot.
|
@greptileai please review the current head 9ed28d9. The storage chain is now built explicitly instead of by excluding credentials. |
|
bugbot run |
There was a problem hiding this comment.
Devin Review found 1 new potential issue.
2 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)
| *((ManagedIdentityCredential(client_id=workload_client_id),) if workload_client_id else ()), | ||
| ManagedIdentityCredential(), |
There was a problem hiding this comment.
🔴 System identity fallback never runs
When AZURE_CLIENT_ID names an unavailable user identity, ChainedTokenCredential stops on its error. The system identity is never attempted, so storage fails
Prompt for agents
The DeploymentIdentityCredential branch creates a ChainedTokenCredential with the client-ID-selected managed identity before the system-assigned managed identity. Azure's ChainedTokenCredential continues only after CredentialUnavailableError. An IMDS response such as "Identity not found" for the selected client ID is an authentication error, so the chain stops and never calls the system-assigned credential. This leaves Azure VMs with a valid system identity unable to use storage whenever the proxy-wide AZURE_CLIENT_ID names a different service identity. Rework this fallback so a missing selected managed identity allows the system-assigned attempt without hiding genuine managed-identity failures. Add a behavioral token-acquisition test where the selected identity returns "Identity not found" and the system identity succeeds.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Tested that exact case on a real VM: an unavailable user identity raises CredentialUnavailableError, so the chain continues and the system identity wins.
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 9ed28d9. Configure here.
4e18c0f
into
litellm_internal_staging

TLDR
Problem this solves:
az loginon the box writes the blobsHow it solves it:
User Flow
Before: an admin turns on Azure Storage logging without storage credentials, and the logs land in the storage account under whichever Azure identity happens to be signed in on the machine
AZURE_STORAGE_ACCOUNT_NAMEandAZURE_STORAGE_FILE_SYSTEMand leaves the threeAZURE_STORAGE_*service principal variables blank, which is what a Helm chart renders for an unset valueaz loginsession, or theAZURE_CLIENT_IDandAZURE_CLIENT_SECRETthe admin set up for Azure OpenAItarget_storage=azure_storageand gets a 200 with a file id, and their uploaded file lands in the same account under the same borrowed identityAfter: the same setup either uses the deployment's own Azure identity or refuses, and nothing else can stand in for it
target_storage=azure_storageand gets a 200, and the file lands under that same identityRelevant issues
Linear ticket
Resolves LIT-6906
Pre-Submission checklist
uv run pytest tests/test_litellm/<your_test_file>.py -vScreenshots / Proof of Fix
Every case below was re-run on 9ed28d9.
Shared setup for every case: real Azure Blob Storage account
litellm2, containerlit6604-e2e,AZURE_STORAGE_ACCOUNT_NAMEandAZURE_STORAGE_FILE_SYSTEMset, the threeAZURE_STORAGE_*service principal variables exported blank, no account key. Chat traffic goes to real Gemini and real Azure OpenAI. Both proxies patch the expired local premium check so the callback runs; the audited code is untouched and the patch is byte identical on both sides. Every bearer token below is decoded off theAuthorizationheader the proxy actually emits, so the identity is observed rather than inferred. The AKS cases run a disposable cluster created with--enable-oidc-issuer --enable-workload-identity, a service account federated to an Entra app that holds Storage Blob Data Contributor, and this branch's three modules overlaid into the stockghcr.io/berriai/litellm:main-latestimage with the sha printed in the pod.Before (1c14ded)
Managed file upload, developer signed in on the host
curl -sS -X POST http://127.0.0.1:20604/v1/files -H "Authorization: Bearer sk-1234" -F purpose=batch -F target_storage=azure_storage -F "file=@batch.jsonl"HTTP 200and{"id":"file-9f33252874a249ea8a57246a","bytes":147,...,"status":"uploaded"}az storage blob list --account-name litellm2 --container-name lit6604-e2e --auth-mode loginlistsa87238a6-5e9d-4a48-9160-90cc1535d4f3.jsonlat 147 bytes{"aud":"https://storage.azure.com","idtyp":"user","upn":"yucheng@berri.ai","appid":"04b07795-8ddb-461a-bbee-02f9e1bf7b46"}, which is the Azure CLI first party app, so the blob was written as a personChat completion logged to Azure Storage, developer signed in on the host
curl -sS -X POST http://127.0.0.1:20604/v1/chat/completions -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"model":"gemini-2.5-flash","messages":[{"role":"user","content":"say the single word ok"}]}'HTTP 200with"content":"ok"from real Geminie90e3354-ec7c-47fa-8759-c3606227eb61.jsonat 10880 bytes, the full prompt and response payload, written under that same human identityManaged file upload, only the Azure OpenAI service principal on the host
AZURE_CONFIG_DIRpointing at an empty directory so noaz loginis reachable, and onlyAZURE_CLIENT_ID,AZURE_CLIENT_SECRETandAZURE_TENANT_IDsetcurl -sS -X POST http://127.0.0.1:20604/v1/files -H "Authorization: Bearer sk-1234" -F purpose=batch -F target_storage=azure_storage -F "file=@batch.jsonl"HTTP 200and{"id":"file-6ea00e5173f14af2b91b8a70",...,"status":"uploaded"}, and the blob listing gainsf868f8f5-79f4-48fa-a0be-e705311ab190.jsonl{"aud":"https://storage.azure.com","idtyp":"app","appid":"0ca1558c-fce6-4be7-b284-e7c77531b863"}, the LLM principalManaged file upload with AZURE_TOKEN_CREDENTIALS=dev on the host
AZURE_TOKEN_CREDENTIALS=dev, which tells the Azure SDK to use developer credentials onlycurl -sS -X POST http://127.0.0.1:20606/v1/files -H "Authorization: Bearer sk-1234" -F purpose=batch -F target_storage=azure_storage -F "file=@batch.jsonl"HTTP 200and{"id":"file-4ca17299827b4be0a7e64a6d",...,"status":"uploaded"}, and the blob listing gains2e497199-b3a9-479c-b7d6-12faa1e30e43.jsonlupn":"yucheng@berri.ai"Azure CLI identityManaged file download
AzureBlobStorageBackend.download_file("https://litellm2.blob.core.windows.net/lit6604-e2e/a87238a6-5e9d-4a48-9160-90cc1535d4f3.jsonl")on the same host{"custom_id":"req-1","method":"POST",...AKS workload identity, every storage secret absent
kubectl applythe probe pod with the base modules overlaid, thenkubectl logs azstor-base -n azstor"has_fix": false, the token{"aud":"https://storage.azure.com","idtyp":"app","appid":"0ca1558c-fce6-4be7-b284-e7c77531b863","oid":"7fc4140a-9e0b-4e11-9688-2ca973ab47b3"}and"uploaded": "azstor-chain-base-1788473984"azstor-chain-base-1788473984.jsonat 70 bytesAKS workload identity with AZURE_TOKEN_CREDENTIALS=dev
AZURE_TOKEN_CREDENTIALS=dev"uploaded": "azstor-chain-head-1788473984", so workload identity federation works under that setting todayAzure OpenAI through Entra ID token refresh
curl -sS -X POST http://127.0.0.1:20608/v1/chat/completions -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"model":"aoai-entra","messages":[{"role":"user","content":"hi"}]}'against the reallitellm-ptu.openai.azure.comHTTP 401from Azure withThe principal 7fc4140a-9e0b-4e11-9688-2ca973ab47b3 lacks the required data action ..., so the token was minted from the full chain and presentedAzure VM on a system assigned managed identity, Azure OpenAI service principal also set
Standard_B2sVM created with--assign-identity, its identity granted Storage Blob Data Contributor on the account, running the stock image with the base modules overlaid, withAZURE_CLIENT_ID,AZURE_CLIENT_SECRETandAZURE_TENANT_IDset for Azure OpenAI{"aud":"https://storage.azure.com","idtyp":"app","appid":"0ca1558c-fce6-4be7-b284-e7c77531b863","oid":"7fc4140a-9e0b-4e11-9688-2ca973ab47b3"}and"uploaded": "sami2-base-sp-1788473364"After (9ed28d9)
Managed file upload, developer signed in on the host
curl -sS -X POST http://127.0.0.1:20605/v1/files -H "Authorization: Bearer sk-1234" -F purpose=batch -F target_storage=azure_storage -F "file=@batch.jsonl"HTTP 500and{"error":{"message":"Internal server error","type":"internal_server_error"}}WorkloadIdentityCredential: ... authentication unavailableandManagedIdentityCredential: ... no response from the IMDS endpointChat completion logged to Azure Storage, developer signed in on the host
curl -sS -X POST http://127.0.0.1:20605/v1/chat/completions -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"model":"gemini-2.5-flash","messages":[{"role":"user","content":"say the single word ok"}]}'HTTP 200with"content":"ok"from real Gemini, so the chat path is unaffectedManaged file upload, only the Azure OpenAI service principal on the host
AZURE_CONFIG_DIRempty and onlyAZURE_CLIENT_ID,AZURE_CLIENT_SECRETandAZURE_TENANT_IDsetcurl -sS -X POST http://127.0.0.1:20605/v1/files -H "Authorization: Bearer sk-1234" -F purpose=batch -F target_storage=azure_storage -F "file=@batch.jsonl"HTTP 500, the blob listing gains nothing, and the log again names only the two deployment credentialsManaged file upload with AZURE_TOKEN_CREDENTIALS=dev on the host
AZURE_TOKEN_CREDENTIALS=devcurl -sS -X POST http://127.0.0.1:20607/v1/files -H "Authorization: Bearer sk-1234" -F purpose=batch -F target_storage=azure_storage -F "file=@batch.jsonl"HTTP 500, the blob listing gains nothing, and the log namesWorkloadIdentityCredentialandManagedIdentityCredential, so the developer only setting cannot empty the chain or reintroduce a developer sign inManaged file download
download_filecall on the same hostClientAuthenticationErrornaming the same two credentials, and no blob bytes are returnedAKS workload identity, every storage secret absent
kubectl logs azstor-head -n azstor"has_fix": true, the same token{"aud":"https://storage.azure.com","idtyp":"app","appid":"0ca1558c-fce6-4be7-b284-e7c77531b863","oid":"7fc4140a-9e0b-4e11-9688-2ca973ab47b3"}and"uploaded": "azstor-chain-head-1788473984"azstor-chain-head-1788473984.jsonat 70 bytes, so workload identity federation is untouchedAKS workload identity with AZURE_TOKEN_CREDENTIALS=dev
kubectl logs azstor-head-atc -n azstor"uploaded": "azstor-chain-head-1788473984", and the blob listing shows that blob at 73 bytes, so a deployment that sets this variable keeps workingAzure OpenAI through Entra ID token refresh
HTTP 401from Azure withPrincipal does not have access to API/Operation., the same authorization refusal against the same principal, so the Azure OpenAI credential chain is unchangedAdmin UI still shows Azure Storage logging turned on
azure_storagewith modeSuccess & Failure, so the callback is registered and running; only the identity it authenticates with changedAzure VM on a system assigned managed identity, Azure OpenAI service principal also set
{"aud":"https://storage.azure.com","idtyp":"app","appid":"b9639c5a-a292-4fef-9038-81816764e0b4","oid":"e048a658-04fd-4bf5-951f-c3d2729a623e"}and"uploaded": "sami2-head-sp-1788473364"AZURE_CLIENT_IDunset the result is the same identity and"uploaded": "sami2-head-noid-1788473364"Reviewer check in the Azure portal
litellm2, then Storage browser, then Blob containers, thenlit6604-e2eazstor-chain-base-1788473984.json,azstor-chain-head-1788473984.jsonandazstor-chain-head-1788473984.jsonfrom the AKS cases,sami2-base-sp-1788473364.jsonandsami2-head-sp-1788473364.jsonfrom the VM cases, and no blob at all from the After cases on the laptopType
🐛 Bug Fix
Caveats (if any)
Severe
AZURE_CLIENT_SECRETno longer reaches storage, useAZURE_STORAGE_CLIENT_*insteadMedium
AZURE_CLIENT_IDnames one identity proxy wideLow
AZURE_CREDENTIAL=DeploymentIdentityCredentialis newly accepted for every caller, previously anAttributeErrorDefaultAzureCredentialsites keep the full chain, listed on LIT-6906Final Attestation
ran /live-pr-risk and found no regressions/backward incompatible risks
Note
Medium Risk
Intentionally breaks keyless storage on machines that relied on CLI or LLM service-principal auth, but changes are scoped to storage token acquisition and are security-motivated.
Overview
Keyless Azure Blob Storage logging and file uploads (when
AZURE_STORAGE_*service principal vars and account key are unset) no longer authenticate withDefaultAzureCredential. They now use a newDeploymentIdentityCredentialmode that only tries workload identity (when federated token env vars are present), then user-assigned managed identity (ifAZURE_CLIENT_IDis set), then system-assigned managed identity.That blocks developer sign-ins (
az login),AZURE_CLIENT_SECRET/ environment-based service principals, and other developer credentials from minting storage tokens—hosts without a deployment identity fail auth instead of writing blobs under the wrong principal.DefaultAzureCredentialand other credential types are unchanged for Azure OpenAI and other callers; dedicated storage SP and account-key paths are untouched.Tests cover the credential chain composition, isolation from
DefaultAzureCredential, and the Azure storage logger wiring.Reviewed by Cursor Bugbot for commit 9ed28d9. Bugbot is set up for automated code reviews on this repo. Configure here.