Three ways a leaked SAS token bites you — user-bound delegation closes one

Three ways a leaked SAS token bites you — user-bound delegation closes one

What does “user-bound” actually add on top of a normal user delegation SAS?

A revocation leash. A standard user delegation SAS is signed with an Entra-issued key instead of the account key, which is a real improvement — but the token itself is still a bearer credential. Whoever holds the string holds the access, until the expiry you baked in. User-bound embeds the object ID of a specific Entra principal into the signature. At access time, Storage evaluates that principal’s RBAC and grants the intersection of the SAS permissions and what the bound identity can actually do right now.

Think of a valet key that only turns the ignition while the named driver’s licence is still valid. Copy the key all you like; it’s dead the moment the DMV pulls the licence.

So it’s no longer a bearer token?

Effectively, no. The three classic SAS failure modes — token pasted into a log, token forwarded to a contractor, token still live after someone left — all shrink to the blast radius of the bound principal’s current permissions. Revoke that principal’s role assignment, or delete the identity, and every user-bound SAS carrying its object ID stops working on the next request. No waiting for expiry. That’s the whole point.

What RBAC does the issuer actually need? (The brief got this wrong.)

The issuer needs the Microsoft.Storage/storageAccounts/blobServices/generateUserDelegationKey/action permission. That action is a documented Blob service operation, and it’s included in the built-in Storage Blob Data Reader, Storage Blob Data Contributor, and Storage Blob Data Owner roles. There is no built-in Azure role called “User Delegation SAS Generator.” If you go looking for one because a slide deck told you it exists, you’ll burn an afternoon. The permission above is the gate — assign one of those data roles, or a custom role carrying that action, and you can issue.

The bound principal needs a real data-plane role on the target, because that RBAC is exactly what gets intersected at read time. Bind to a principal with no access and you’ve minted a token that authorises nothing. Slightly poetic, entirely useless.

How does Storage verify the match at access time?

It doesn’t ask the caller to re-authenticate as the principal. It reads the object ID out of the signature, resolves that identity’s current role assignments, and evaluates the request against the intersection. This is the elegant bit: the check is live, not baked into the token at issue time. Downscope a Contributor to Reader an hour after issuing, and a write on that SAS starts returning 403 immediately.

Does the bound service principal need special claims or config?

No custom claims, no app-registration surgery. It needs a stable object ID and the data-plane role. Managed identities work well as bound principals — arguably the best fit, since there’s no secret to leak alongside the token.

Show me the az CLI command.

Here’s the honest answer: as of GA, Microsoft has not published az CLI syntax or a parameter name for the binding on the update entry. I’m not going to hand you an invented flag that fails on paste. Conceptually the call is a user delegation SAS (--as-user forces the delegation signing path) plus one new input — the delegated principal’s object ID, which surfaces in the signed URL as an additional signed-object-ID field:

run.shbash — zsh
# SKELETON — the binding parameter name is NOT yet in published az docs.
# Confirm against `az storage container generate-sas --help` on your build.
BOUND_OID=$(az ad sp show --id "$APP_ID" --query id -o tsv)
​
az storage container generate-sas \
  --account-name mystorageacct \
  --name deliverables \
  --permissions rl \
  --expiry 2026-09-11T00:00Z \
  --auth-mode login \
  --as-user \
  -o tsv
# ...plus the delegated-principal object ID input, once documented.

Drop the binding entirely and you’re back to an ordinary bearer SAS. And no — I can’t confirm from the GA notice whether this needs the storage-preview extension or ships in the core module. Check az storage --help on your installed version before you wire it into a pipeline.

Blob, file, queue — all three?

The user delegation key is a Blob service operation — the permission literally lives under blobServices. That makes Blob the surface, full stop. File shares use a different Entra path (identity-based authentication), and Table isn’t part of the user delegation model at all. If the update entry lists queue or file support at GA, treat that as news to verify against the page — the permission model doesn’t hand it to you for free.

Which regions and SKUs?

User delegation itself needs a standard general-purpose v2 or premium block blob account with Entra data-plane auth — classic and legacy accounts are excluded. GA rarely means every region on day one, so confirm your target region on the update page before you write it into a landing-zone policy. Don’t promise your platform team global availability you haven’t watched light up.

Should I rip out my existing user delegation SAS?

Where a token flows to a known identity — a service, a specific user — bind it. Where you genuinely can’t name the presenter, you were probably reaching for the wrong tool anyway. The uncomfortable truth is that most of your “temporary” SAS tokens outlive the reason you issued them. This finally gives that sprawl an off switch that isn’t a calendar.