Skip to content

CLO-190: persist: refresh Azure workload identity tokens from rotated file - #38178

Open
jubrad wants to merge 3 commits into
MaterializeInc:mainfrom
jubrad:justin/clo-190-sgfleet-token-expiration-issue-with-azure-object-storage
Open

CLO-190: persist: refresh Azure workload identity tokens from rotated file#38178
jubrad wants to merge 3 commits into
MaterializeInc:mainfrom
jubrad:justin/clo-190-sgfleet-token-expiration-issue-with-azure-object-storage

Conversation

@jubrad

@jubrad jubrad commented Aug 12, 2026

Copy link
Copy Markdown
Member

Fixes CLO-190.

Seems to work on an Azure env I spun up.

Motivation

A customer environment on AKS lost persist auth into Azure Blob Storage after running for a while. Root cause: azure_identity 0.21's default credential chain resolves AKS workload identity via WorkloadIdentityCredential, which reads the projected service account token from AZURE_FEDERATED_TOKEN_FILE once at construction and holds the contents for the life of the process. Kubernetes rotates that projected token, so once the last cached AAD access token expired, every refresh presented a stale client assertion to AAD and failed, permanently locking long-running processes out of persist blob storage.

Changes

  • Add a RefreshingWorkloadIdentityCredential in src/persist/src/azure.rs that re-reads the projected token file on every AAD access token refresh and exchanges it via the public azure_identity::federated_credentials_flow::perform. Resulting access tokens are cached per scope until near expiry, mirroring azure_identity's internal token cache semantics (including its 20s expiry buffer), with refresh under a write lock to avoid stampeding AAD.
  • Use it in AzureBlobConfig::new when AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_FEDERATED_TOKEN_FILE are all present (and AZURE_FEDERATED_TOKEN is not, that static-token path keeps its upstream precedence). Otherwise fall back to create_default_credential(), whose remaining credential types (e.g. IMDS managed identity) refresh correctly.
  • The SAS-token-in-URL path is unchanged. The stale TODO about refreshing credentials is replaced with a NOTE documenting the remaining limitation: a SAS token passed via the URL is static and must outlive the process.
  • Unit test covers token-file re-read on refresh, cache reuse before expiry, whitespace trimming, and clear_cache, without contacting AAD (the exchange is injected).

Out of scope

Migrating off the archived unofficial azure_storage/azure_identity 0.21 crates to the official Azure SDK for Rust is the long-term fix for this class of issue, but is a much larger change and not needed to resolve this incident class.

Tips for reviewer

Compare with the vendored azure_identity 0.21 sources: WorkloadIdentityCredential::create reads the token file once (token_credentials/workload_identity_credentials.rs), and it enters the default chain via EnvironmentCredential (token_credentials/environment_credentials.rs).

Checklist

  • This PR has adequate test coverage / QA involvement has been duly considered. (trigger-ci for additional test/nightly runs)
  • This PR has an associated up-to-date design doc, is a design doc (template), or is sufficiently small to not require a design.
  • If this PR evolves an existing $T ⇔ Proto$T mapping (possibly in a backwards-incompatible way), then it is tagged with a T-proto label.
  • If this PR will require changes to cloud orchestration or tests, there is a companion cloud PR to account for those changes that is tagged with the release-blocker label (example).
  • If this PR includes major user-facing behavior changes, I have pinged the relevant PM to schedule a changelog post.

🤖 Generated with Claude Code

jubrad and others added 3 commits August 12, 2026 09:13
The azure_identity default credential chain resolves AKS workload
identity via a WorkloadIdentityCredential that reads
AZURE_FEDERATED_TOKEN_FILE once at construction and holds the contents
for the life of the process. Kubernetes rotates that projected token,
so once the last cached AAD access token expired, every refresh
presented a stale client assertion and failed, permanently locking
long-running processes out of persist blob storage.

Add a RefreshingWorkloadIdentityCredential that re-reads the projected
token file on every AAD access token refresh and caches the resulting
access token until near expiry, mirroring azure_identity's token cache
semantics. Use it when the workload identity environment variables are
present, falling back to the default credential chain (whose remaining
credential types, e.g. IMDS managed identity, refresh correctly)
otherwise. The SAS-token-in-URL path is unchanged.

Fixes CLO-190.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A token provided directly via the AZURE_FEDERATED_TOKEN env var is
static, so there is nothing to re-read on refresh. azure_identity's
credential chain prefers it over AZURE_FEDERATED_TOKEN_FILE, so
preserve that precedence when both are set.

Part of CLO-190.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Part of CLO-190.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant