Skip to main content

Theoretical Foundation: Configure Stored Access Policies


1. Initial Intuition​

In the previous module, we learned that a SAS Token is a signed URL with permissions and expiration embedded directly in the token. Once issued, this token cannot be changed: if you granted read access for 30 days and need to revoke it on day 10, the only way out is to rotate the Account Key, which disrupts all other accesses that use that key.

Imagine you issue access cards for a commercial building. Each card has the expiration date printed on plastic, impossible to erase. If a visitor loses the card on day 3 of a 30-day valid access, you can't invalidate that specific card without changing the entire building's lock (which affects all other cards).

A Stored Access Policy (SAP) solves exactly this: instead of printing permissions and expiration directly on the SAS Token, you store these definitions in a centralized record within the container itself (or queue, table, file share), and the SAS Token only references this record by name. If you need to revoke or change permissions, you modify or delete the central record, and all SAS tokens that reference it are affected immediately, without needing to touch the Account Keys or the tokens themselves.


2. Context​

Where Stored Access Policies fit in the SAS ecosystem​

100%
Scroll para zoom Β· Arraste para mover Β· πŸ“± Pinch para zoom no celular

Why Stored Access Policies exist​

The need arises from a governance problem: SAS Tokens are access delegation instruments that, by their cryptographic nature, cannot be individually revoked without systemic impact (Account Key rotation).

In real scenarios, this is a serious problem:

  • An external partner receives a 90-day SAS and the contract is terminated on day 15
  • A token is accidentally exposed in logs or source code
  • An application needs to have its permissions reduced without service interruption

SAPs create a centralized control point that decouples token issuance from control of its permissions.

What depends on SAPs​

  • Long-duration B2B integrations that need selective revocation
  • Environments with multiple partners each having different access to the same container
  • Audit systems that need to track which accesses are active
  • Compliance scenarios where third-party access needs to be removed quickly

3. Building the Concepts​

3.1 Structure of a Stored Access Policy​

A SAP is an object stored within a storage resource with four fields:

100%
Scroll para zoom Β· Arraste para mover Β· πŸ“± Pinch para zoom no celular

**The `id` field** is the unique identifier of the SAP within the resource. It's the name that the SAS Token references in the `si` (signedIdentifier) parameter.

**Permission and expiration fields can be omitted from the SAP**, as long as they are provided in the SAS Token itself. However, if they are in the SAP, they control the effective access.

### 3.2 Where SAPs can be created

| Resource | Supports SAP | Maximum number of SAPs |
|---|---|---|
| **Container (Blob)** | Yes | 5 |
| **Individual Blob** | No | N/A |
| **Queue** | Yes | 5 |
| **Table** | Yes | 5 |
| **File Share** | Yes | 5 |
| **Storage Account (level)** | No | N/A |

The limit of **5 SAPs per resource** is fixed and cannot be increased. This requires careful planning for organizations with many partners accessing the same container.

> **Attention:** SAPs exist only for **Service SAS**. It's not possible to create a SAP for Account SAS or User Delegation SAS.

### 3.3 How a SAS references a SAP

The `si` (signedIdentifier) parameter in the SAS Token contains the SAP name:

https://stgprod001.blob.core.windows.net/container1/arquivo.pdf ?sv=2022-11-02 &sr=b &si=policy-parceiro-externo ← SAP name &sig=abc123... ← signature calculated including the SAP name


When storage receives this request:
1. Verifies the signature
2. Looks for the SAP called `policy-parceiro-externo` in container `container1`
3. Applies the SAP's permissions and expiration
4. Decides if the operation is allowed

### 3.4 Conflict resolution: SAP vs. SAS

When both the SAP and SAS Token define permissions or expiration, **the most restrictive prevails**:

```mermaid
flowchart TD
A["SAS references SAP 'policy-A'"] --> B{"SAP defines\npermissions?"}
B -->|Yes| C{"SAS also\ndefines permissions?"}
C -->|Yes| D["Uses intersection\n(more restrictive)"]
C -->|No| E["Uses SAP permissions"]
B -->|No| F{"SAS defines\npermissions?"}
F -->|Yes| G["Uses SAS permissions"]
F -->|No| H["Error: at least one\nmust define permissions"]

A --> I{"SAP defines\nexpiration?"}
I -->|Yes| J{"SAS also\ndefines expiration?"}
J -->|Yes| K["Uses shorter expiration\n(more restrictive)"]
J -->|No| L["Uses SAP expiration"]
I -->|No| M{"SAS defines\nexpiration?"}
M -->|Yes| N["Uses SAS expiration"]
M -->|No| O["No expiration: valid indefinitely\n(not recommended)"]

style D fill:#d32f2f,stroke:#b71c1c,color:#fff
style K fill:#d32f2f,stroke:#b71c1c,color:#fff
style H fill:#d32f2f,stroke:#b71c1c,color:#fff

This "most restrictive prevails" rule is important to understand the behavior when SAPs are updated.

3.5 Revocation mechanism via SAP​

100%
Scroll para zoom Β· Arraste para mover Β· πŸ“± Pinch para zoom no celular

This is the central value flow of SAPs: instant and surgical revocation, without affecting other tokens or services.


4. Structural View​

How SAPs coexist in a container​

100%
Scroll para zoom Β· Arraste para mover Β· πŸ“± Pinch para zoom no celular

Note that policy-auditoria has empty permissions and dates in the SAP: the expiration is provided in the SAS Token that references it. This is valid, but means the SAP alone doesn't control anything beyond the identifier name. Revocation is still possible: if you delete the SAP, the SAS stops working even if the token's date hasn't expired yet.


5. Practical Operation​

Lifecycle of a Stored Access Policy​

100%
Scroll para zoom Β· Arraste para mover Β· πŸ“± Pinch para zoom no celular

Critical non-obvious behaviors​

Modifying a SAP can take effect in up to 30 seconds. After creating, modifying, or deleting a SAP, Azure needs up to 30 seconds to propagate the change. During this period, behavior may be inconsistent: some Azure servers already recognize the change, others don't yet. In urgent security operations (revoking access from compromised partner), wait 30 seconds after deleting the SAP before considering access effectively blocked.

An empty SAP (without permissions or expiration) still has value. A SAP with only the id (name), without permissions and without expiration, still serves as a revocation anchor: if you delete this SAP, all SAS tokens that reference it stop working, even if the SAS Token itself still has valid expiration date and permissions defined in it.

Updating a SAP doesn't create a new version. When modifying permissions or expiration of an existing SAP, Azure replaces the previous definition. There's no version history. Already issued SAS Tokens that reference that SAP start using the new configurations immediately (after propagation of up to 30 seconds).

Deleting and recreating a SAP with the same name is different from modifying. If you delete policy-parceiro-a and create a new SAP with the same name, SAS Tokens that referenced the previous policy continue working, as they reference by name. This means deleting and recreating with the same name doesn't revoke existing tokens.

SAPs in containers don't apply to individual blobs with SAS. If you create a SAP in the container and issue a Service SAS with sr=b (resource = blob), referencing the container's SAP, it works. But SAPs are stored in the container, not in the individual blob. This is expected: the SAP is an attribute of the container.


6. Implementation Methods​

Azure Portal​

When to use: point management, visual verification of SAP status

The Azure portal doesn't have a direct interface to manage Stored Access Policies in blob containers. To manage container SAPs via graphical interface, you need to use Azure Storage Explorer (Microsoft's free desktop application).

In the portal, SAPs can be indirectly verified when generating a container SAS Token, where existing policies appear as an option.

Portal limitation: no complete SAP management via browser; requires Storage Explorer or CLI/PowerShell.


Azure CLI​

# Create SAP in a container (with permissions and expiration)
az storage container policy create \
--account-name "stgprod001" \
--account-key "<account-key>" \
--container-name "dados-parceiros" \
--name "policy-parceiro-a" \
--permissions r \
--start "2026-03-24T00:00:00Z" \
--expiry "2026-06-30T23:59:59Z"

# Create SAP with only the name (revocation anchor)
az storage container policy create \
--account-name "stgprod001" \
--account-key "<account-key>" \
--container-name "dados-parceiros" \
--name "policy-auditoria"
# Without --permissions, --start, --expiry: valid SAP without own restrictions

# List all SAPs in a container
az storage container policy list \
--account-name "stgprod001" \
--account-key "<account-key>" \
--container-name "dados-parceiros" \
--output table

# View details of a specific SAP
az storage container policy get \
--account-name "stgprod001" \
--account-key "<account-key>" \
--container-name "dados-parceiros" \
--name "policy-parceiro-a"

# MODIFY an existing SAP (replaces in-place)
az storage container policy update \
--account-name "stgprod001" \
--account-key "<account-key>" \
--container-name "dados-parceiros" \
--name "policy-parceiro-a" \
--permissions rl \
--expiry "2026-09-30T23:59:59Z"
# Permissions expanded from r to rl and expiration extended

# REVOKE: delete the SAP immediately invalidates all SAS that reference it
az storage container policy delete \
--account-name "stgprod001" \
--account-key "<account-key>" \
--container-name "dados-parceiros" \
--name "policy-parceiro-a"

# Generate Service SAS referencing a SAP
az storage container generate-sas \
--account-name "stgprod001" \
--account-key "<account-key>" \
--name "dados-parceiros" \
--policy-name "policy-parceiro-a" \
--https-only \
--output tsv

# Generate blob SAS referencing container SAP
az storage blob generate-sas \
--account-name "stgprod001" \
--account-key "<account-key>" \
--container-name "dados-parceiros" \
--name "relatorio-q1.pdf" \
--policy-name "policy-parceiro-a" \
--https-only \
--output tsv

# SAP in a Queue
az storage queue policy create \
--account-name "stgprod001" \
--account-key "<account-key>" \
--queue-name "fila-processos" \
--name "policy-processador" \
--permissions raup \
--expiry "2026-12-31T23:59:59Z"

# SAP in a File Share
az storage share policy create \
--account-name "stgprod001" \
--account-key "<account-key>" \
--share-name "compartilhamento-externo" \
--name "policy-leitura-share" \
--permissions rl \
--expiry "2026-06-30T23:59:59Z"

# SAP in a Table
az storage table policy create \
--account-name "stgprod001" \
--account-key "<account-key>" \
--table-name "dadosoperacionais" \
--name "policy-leitura-tabela" \
--permissions r \
--expiry "2026-12-31T23:59:59Z"

Azure PowerShell​

# Create storage context
$ctx = New-AzStorageContext `
-StorageAccountName "stgprod001" `
-StorageAccountKey "<account-key>"

# Create SAP in container
New-AzStorageContainerStoredAccessPolicy `
-Context $ctx `
-Container "dados-parceiros" `
-Policy "policy-parceiro-a" `
-Permission "r" `
-StartTime (Get-Date "2026-03-24") `
-ExpiryTime (Get-Date "2026-06-30T23:59:59Z")

# List SAPs in a container
Get-AzStorageContainerStoredAccessPolicy `
-Context $ctx `
-Container "dados-parceiros" |
Select-Object Policy, Permissions, SharedAccessStartTime, SharedAccessExpiryTime

# Modify existing SAP
Set-AzStorageContainerStoredAccessPolicy `
-Context $ctx `
-Container "dados-parceiros" `
-Policy "policy-parceiro-a" `
-Permission "rl" `
-ExpiryTime (Get-Date "2026-09-30T23:59:59Z")

# Delete SAP (revokes all SAS that reference it)
Remove-AzStorageContainerStoredAccessPolicy `
-Context $ctx `
-Container "dados-parceiros" `
-Policy "policy-parceiro-a"

# Generate SAS referencing SAP
$sas = New-AzStorageContainerSASToken `
-Context $ctx `
-Name "dados-parceiros" `
-Policy "policy-parceiro-a"

# Complete URL with SAS
$containerUrl = "https://stgprod001.blob.core.windows.net/dados-parceiros$sas"

# SAP in Queue
New-AzStorageQueueStoredAccessPolicy `
-Context $ctx `
-Queue "fila-processos" `
-Policy "policy-processador" `
-Permission "raup" `
-ExpiryTime (Get-Date).AddMonths(9)

# SAP in File Share
New-AzStorageShareStoredAccessPolicy `
-Context $ctx `
-ShareName "compartilhamento-externo" `
-Policy "policy-leitura-share" `
-Permission "rl" `
-ExpiryTime (Get-Date).AddMonths(3)

Bicep (ARM Template)​

SAPs can be defined as part of the container resource in ARM/Bicep templates:

resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: 'stgprod001'
location: 'brazilsouth'
kind: 'StorageV2'
sku: { name: 'Standard_LRS' }
properties: {}
}

resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = {
parent: storageAccount
name: 'default'
}

resource container 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = {
parent: blobService
name: 'dados-parceiros'
properties: {
publicAccess: 'None'
}
}

// SAPs are defined via separate operation in ARM
// No direct support in Bicep for SAPs in containers
// Use deployment scripts or post-deploy via CLI
resource deploymentScript 'Microsoft.Resources/deploymentScripts@2023-08-01' = {
name: 'create-sap-parceiro-a'
location: 'brazilsouth'
kind: 'AzureCLI'
properties: {
azCliVersion: '2.50.0'
retentionInterval: 'P1D'
scriptContent: '''
az storage container policy create \
--account-name ${STORAGE_NAME} \
--container-name dados-parceiros \
--name policy-parceiro-a \
--permissions r \
--expiry 2026-06-30T23:59:59Z \
--auth-mode login
'''
environmentVariables: [
{ name: 'STORAGE_NAME', value: storageAccount.name }
]
}
dependsOn: [container]
}

SAPs don't have native support as standalone ARM resources. The recommended approach for IaC is to use Deployment Scripts or configure them via post-deploy pipeline.


7. Control and Security​

Granular revocation strategy​

With SAPs, you can have multiple partners accessing the same container with independent revocation:

100%
Scroll para zoom Β· Arraste para mover Β· πŸ“± Pinch para zoom no celular

The 5 SAPs limit and how to manage it​

When you have more than 5 partners or use cases, the options are:

  1. Consolidate partners in shared SAPs: if two partners have the same permissions and expiration, they use the same SAP. Their SAS Token is different (signed at different times), but the SAP is the same.

  2. Segregate into multiple containers: instead of a single container with 5 SAPs, use separate containers per partner or partner group.

  3. Accept SAS without SAP for low-risk cases: partners with short-duration access (hours) don't need SAP, as access expires naturally.

  4. Use User Delegation SAS: for partners with Azure AD identity, eliminates the need for SAP completely.

Auditing active SAPs​

# Complete report of SAPs in all containers of a storage account
for container in $(az storage container list \
--account-name "stgprod001" \
--account-key "<account-key>" \
--query "[].name" -o tsv); do
echo "=== Container: $container ==="
az storage container policy list \
--account-name "stgprod001" \
--account-key "<account-key>" \
--container-name "$container" \
--output table
done

8. Decision Making​

When to use SAP vs. ad hoc SAS​

SituationChoiceReason
Few hours access for file uploadAd hoc SASAutomatic expiration makes SAP unnecessary
6-month contract with external partnerSAPPossibility of early termination requires revocation
Multiple partners with different permissions in the same containerSeparate SAPs per partnerGranular revocation per partner
Internal pipeline with Managed IdentityRBAC (no SAS)Managed Identity is more secure than any SAS
External partner with recurring read accessSAP without expiration in SAP, with expiration in SASIssue short-duration SAS that reference long-duration SAP; revoke via SAP if needed
5 SAPs limit reachedCreate additional containerNo way to increase the limit
Individual blob access per hourAd hoc blob SASSAP not necessary for such short duration

SAS with SAP vs. SAS without SAP: structural comparison​

AspectAd hoc SAS (no SAP)SAS with SAP
Revocation before expirationOnly by rotating Account KeyDelete the SAP
Revocation impactAll SAS with that keyOnly SAS referencing that SAP
Maximum expirationDetermined at creationCan be indefinite (expiration in SAS)
Active access auditingDifficult (scattered tokens)Possible via SAP listing
Modify permissions without revokingImpossibleModify the SAP (affects immediately)
Implementation complexityLowMedium
Ideal forTemporary access of hoursAccess of days, weeks or months

9. Best Practices​

Use one SAP per partner/external system, not per access type. Naming SAPs by partner (not by permission) facilitates selective revocation. policy-partner-acme is more manageable than policy-read shared by five partners.

Keep an external record of which SAS references which SAP. SAPs are stored in storage, but there's no way to know which tokens were issued against them. Keep a spreadsheet or database: who received, which SAP, when generated, expected validity date. This is essential to know who is affected when you need to revoke.

Prefer putting expiration in SAP, not just in SAS. If expiration is in the SAP, you can extend it without needing to issue new tokens to the partner. If it's only in the SAS, when the SAS expires you need to issue a new token and deliver it to the partner.

Use SAPs for any SAS with duration above 24 hours. One day is the reasonable practical limit: if something goes wrong in less than 24 hours, waiting for natural expiration is acceptable. For anything beyond that, the risk of needing to revoke justifies using SAP.

Name SAPs with business context, not technical. policy-partner-acme-read-2026 is much better than p1 or sap-r. The name is the only identifier in logs and audits.

Document the revocation process before needing it. In security incident situations, executing the correct procedure under pressure is difficult. Have a documented runbook: "to revoke partner X access: execute az storage container policy delete...". This reduces critical response time.


10. Common Errors​

ErrorWhy it happensHow to avoid
Delete and recreate SAP with same name expecting to revoke accessNot understanding that the name is the identifier; recreating restores accessTo revoke, delete without recreating (or create with different name)
Reaching 5 SAPs limit without planningCreating ad hoc SAPs without structurePlan SAPs before having partners; consolidate where possible
SAP modified but access still works for 30 secondsNot knowing the propagation delayWait 30 seconds after critical changes
Empty SAP (no fields) doesn't revoke access by itselfConfusing SAP as blocking mechanism; empty SAP grants indefinite accessUnderstand that revocation occurs by DELETING the SAP, not by emptying it
SAS generated without referencing SAP for long-duration partnerHaste, not planning for revocationUse SAP for any external partner with access > 24h
Not documenting which SAS were issued against each SAPTrusting memory or informal communicationKeep centralized external record
Too long expiration in SAP without SAP as revocation anchorOptimism about partnership maintenanceAlways have SAP as control mechanism, regardless of expiration

The most dangerous error​

Believing that modifying a SAP's permissions (e.g., from r to ``) will block access. There's no empty permission that means "deny all". To block access, the SAP needs to be deleted, not emptied. A SAP with empty fields results in undefined behavior depending on how the SAS that references it was configured.


11. Operation and Maintenance​

SAPs review routine​

# Check expired SAPs that still exist (cleanup)
CURRENT_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
az storage container policy list \
--account-name "stgprod001" \
--account-key "<account-key>" \
--container-name "dados-parceiros" \
--query "[?expiry != null && expiry < '$CURRENT_DATE'].{Name: id, Expired_on: expiry}" \
--output table

# List SAPs expiring in the next 30 days (for proactive renewal)
EXPIRY_THRESHOLD=$(date -u -d '+30 days' +%Y-%m-%dT%H:%M:%SZ)
az storage container policy list \
--account-name "stgprod001" \
--account-key "<account-key>" \
--container-name "dados-parceiros" \
--query "[?expiry != null && expiry < '$EXPIRY_THRESHOLD' && expiry > '$CURRENT_DATE'].{Name: id, Expires_on: expiry}" \
--output table

# Complete audit script: SAPs in all containers of all storage accounts
for storage in $(az storage account list --query "[].name" -o tsv); do
KEY=$(az storage account keys list \
--account-name "$storage" \
--query "[0].value" -o tsv 2>/dev/null)

if [ -n "$KEY" ]; then
echo "=== Storage: $storage ==="
for container in $(az storage container list \
--account-name "$storage" \
--account-key "$KEY" \
--query "[].name" -o tsv 2>/dev/null); do
POLICIES=$(az storage container policy list \
--account-name "$storage" \
--account-key "$KEY" \
--container-name "$container" \
--output tsv 2>/dev/null)
if [ -n "$POLICIES" ]; then
echo " Container: $container"
echo "$POLICIES"
fi
done
fi
done

Limits to monitor​

LimitValueImpact
SAPs per container5Mandatory planning for multiple partners
SAPs per queue5Same
SAPs per table5Same
SAPs per file share5Same
Propagation delayUp to 30 secondsConsider in urgent security operations
Maximum SAP name size64 charactersDescriptive names fit easily

12. Integration and Automation​

Pattern: Partner access management via internal portal​

100%
Scroll para zoom Β· Arraste para mover Β· πŸ“± Pinch para zoom no celular

Automation for rotating SAPs near expiration​

# Runbook: Notify administrators about SAPs expiring in 7 days
$ctx = New-AzStorageContext -StorageAccountName "stgprod001" -StorageAccountKey "<key>"

$containers = Get-AzStorageContainer -Context $ctx
$expiringPolicies = @()

foreach ($container in $containers) {
$policies = Get-AzStorageContainerStoredAccessPolicy -Context $ctx -Container $container.Name

foreach ($policy in $policies) {
if ($policy.SharedAccessExpiryTime -ne $null) {
$daysUntilExpiry = ($policy.SharedAccessExpiryTime - (Get-Date)).Days
if ($daysUntilExpiry -le 7 -and $daysUntilExpiry -ge 0) {
$expiringPolicies += [PSCustomObject]@{
Container = $container.Name
Policy = $policy.Policy
ExpiresIn = "$daysUntilExpiry days"
ExpiryDate = $policy.SharedAccessExpiryTime
}
}
}
}
}

if ($expiringPolicies.Count -gt 0) {
$body = $expiringPolicies | ConvertTo-Html -Property Container, Policy, ExpiresIn, ExpiryDate
# Send email or Teams notification with $body
Write-Output "Policies expiring in 7 days:"
$expiringPolicies | Format-Table
} else {
Write-Output "No SAPs expiring in the next 7 days."
}

13. Final Summary​

Essential points:

  • Stored Access Policy (SAP) is an access policy stored in the resource itself (container, queue, table, file share) that Service SAS Tokens can reference by name
  • The main function of SAP is to enable granular revocation: deleting the SAP invalidates all SAS that reference it, without affecting Account Keys or other accesses
  • SAPs only work with Service SAS; don't apply to Account SAS or User Delegation SAS
  • The limit is 5 SAPs per resource (container, queue, table, file share); fixed and non-configurable limit
  • After creating, modifying or deleting a SAP, propagation takes up to 30 seconds

Critical differences:

  • SAP vs. ad hoc SAS: ad hoc SAS has embedded permissions and is irrevocable; SAS with SAP has permissions in external policy and is revocable via SAP deletion
  • Delete SAP vs. modify SAP: deleting revokes all SAS that reference; modifying changes permissions/expiration but keeps SAS working with new parameters
  • Empty SAP vs. deleted SAP: empty SAP still exists as anchor and SAS referencing it continue working (with permissions/expiration from the SAS Token itself); deleted SAP invalidates all SAS immediately
  • Delete and recreate SAP with same name vs. revoke: recreating with same name restores access for existing SAS; to permanently revoke, don't recreate

What needs to be remembered for AZ-104:

  • SAPs exist only for Service SAS
  • The parameter in SAS Token that references the SAP is si (signedIdentifier)
  • When there's conflict between SAP and SAS Token, the most restrictive prevails (shorter expiration, smaller permission set)
  • Maximum of 5 SAPs per resource (container, queue, table, file share)
  • SAPs don't exist at storage account level (only in individual resources within it)
  • The propagation delay after SAP changes is up to 30 seconds
  • SAP names have maximum size of 64 characters