Lab 02: Storage Account, SAS Tokens, and Lifecycle Policies
Estimated time: 45–60 minutes Difficulty: ⭐⭐☆☆☆ Environment: Azure free account — portal + Azure CLI
Prerequisites
- Azure CLI ≥ 2.50 with
az logincomplete az storageextension available (bundled with core CLI)
az account show --query "{name:name, id:id}" -o table
Lab Objectives
- Create a GPv2 storage account with specific redundancy and access tier
- Upload blobs and change access tiers manually
- Configure a lifecycle management policy for automatic tiering
- Generate a Service SAS token with limited permissions
- Create a Stored Access Policy and link a SAS to it
- Revoke the SAP-linked SAS without touching the account key
- Mount an Azure File share on macOS/Linux (bonus)
Step 1: Create the Storage Account
RG="rg-az104-lab02"
LOCATION="eastus"
# Storage account names: 3-24 chars, lowercase letters and numbers only
SA="staz104lab$(openssl rand -hex 3)"
az group create --name $RG --location $LOCATION
az storage account create \
--name $SA \
--resource-group $RG \
--location $LOCATION \
--sku Standard_GRS \
--kind StorageV2 \
--access-tier Hot \
--https-only true \
--min-tls-version TLS1_2
echo "Storage account: $SA"
# Verify
az storage account show \
--name $SA \
--resource-group $RG \
--query "{sku:sku.name, tier:accessTier, kind:kind, tls:minimumTlsVersion}" \
-o table
Why
--https-only trueand--min-tls-version TLS1_2? These are exam-common hardening settings and required for most compliance frameworks. Both are defaults in new accounts but worth knowing how to set explicitly.
Step 2: Create a Container and Upload Blobs
# Get the account key for subsequent operations
ACCT_KEY=$(az storage account keys list \
--account-name $SA \
--resource-group $RG \
--query "[0].value" -o tsv)
# Create a container (private access — no anonymous reads)
az storage container create \
--name "logs" \
--account-name $SA \
--account-key $ACCT_KEY \
--public-access off
# Create some test files and upload
echo "Log entry $(date)" > log1.txt
echo "Log entry $(date)" > log2.txt
echo "Archive data" > archive.txt
az storage blob upload \
--account-name $SA \
--account-key $ACCT_KEY \
--container-name "logs" \
--name "2024/jan/log1.txt" \
--file log1.txt \
--tier Hot
az storage blob upload \
--account-name $SA \
--account-key $ACCT_KEY \
--container-name "logs" \
--name "2024/jan/log2.txt" \
--file log2.txt \
--tier Hot
az storage blob upload \
--account-name $SA \
--account-key $ACCT_KEY \
--container-name "logs" \
--name "2023/archive.txt" \
--file archive.txt \
--tier Hot
# List blobs and verify tiers
az storage blob list \
--account-name $SA \
--account-key $ACCT_KEY \
--container-name "logs" \
--query "[].{name:name, tier:properties.blobTier}" \
-o table
Step 3: Manually Change a Blob's Access Tier
Change the archive blob to Cool tier:
az storage blob set-tier \
--account-name $SA \
--account-key $ACCT_KEY \
--container-name "logs" \
--name "2023/archive.txt" \
--tier Cool
# Verify
az storage blob show \
--account-name $SA \
--account-key $ACCT_KEY \
--container-name "logs" \
--name "2023/archive.txt" \
--query "properties.blobTier" -o tsv
⚠️ Tricky spot: Setting a blob to Archive makes it offline immediately. If you set it to Archive by mistake, you must rehydrate before you can read it — this takes up to 15 hours on Standard priority.
Step 4: Configure a Lifecycle Management Policy
Create a policy that:
- Moves blobs in the
logs/container to Cool after 30 days - Moves them to Archive after 90 days
- Deletes them after 365 days
cat > lifecycle-policy.json << 'EOF'
{
"rules": [
{
"name": "log-lifecycle",
"enabled": true,
"type": "Lifecycle",
"definition": {
"filters": {
"blobTypes": ["blockBlob"],
"prefixMatch": ["logs/2024/"]
},
"actions": {
"baseBlob": {
"tierToCool": {
"daysAfterModificationGreaterThan": 30
},
"tierToArchive": {
"daysAfterModificationGreaterThan": 90
},
"delete": {
"daysAfterModificationGreaterThan": 365
}
}
}
}
}
]
}
EOF
az storage account management-policy create \
--account-name $SA \
--resource-group $RG \
--policy @lifecycle-policy.json
# Verify the policy was applied
az storage account management-policy show \
--account-name $SA \
--resource-group $RG \
--query "policy.rules[].{name:name, prefix:definition.filters.prefixMatch}" \
-o table
Lifecycle policies run once per day. You won't see tier changes instantly in the lab — but you will be tested on the JSON structure and the
daysAfterModificationGreaterThanlogic in the exam.
Step 5: Create a Service SAS Token
Generate a SAS token scoped to the logs container with read-only permission, expiring in 1 hour:
# SAS expiry: 1 hour from now
EXPIRY=$(date -u -v+1H "+%Y-%m-%dT%H:%MZ" 2>/dev/null || date -u -d "+1 hour" "+%Y-%m-%dT%H:%MZ")
SAS_TOKEN=$(az storage container generate-sas \
--account-name $SA \
--account-key $ACCT_KEY \
--name "logs" \
--permissions "rl" \
--expiry $EXPIRY \
--https-only \
--output tsv)
echo "SAS Token: $SAS_TOKEN"
# Construct the full URL for a specific blob
BLOB_URL="https://$SA.blob.core.windows.net/logs/2024/jan/log1.txt?$SAS_TOKEN"
echo "Full URL: $BLOB_URL"
# Test read access using the SAS
curl -s "$BLOB_URL"
Test that write is denied:
# Try to upload with a read-only SAS — should fail with 403
az storage blob upload \
--account-name $SA \
--sas-token $SAS_TOKEN \
--container-name "logs" \
--name "unauthorized-write.txt" \
--file log1.txt
# Expected: AuthorizationPermissionMismatch
⚠️ Tricky spot: SAS permissions are a string of letters (
r=read,w=write,d=delete,l=list,a=add,c=create). They are not validated for logic conflicts — you can accidentally give too many permissions if you're not careful.
Step 6: Create a Stored Access Policy and Revoke It
A Stored Access Policy (SAP) lets you revoke SAS tokens by modifying the policy — without rotating the account key.
# Create a Stored Access Policy on the container
az storage container policy create \
--account-name $SA \
--account-key $ACCT_KEY \
--container-name "logs" \
--name "read-policy" \
--permissions "rl" \
--expiry $EXPIRY
# Create a SAS that references this policy
SAP_SAS=$(az storage container generate-sas \
--account-name $SA \
--account-key $ACCT_KEY \
--name "logs" \
--policy-name "read-policy" \
--https-only \
--output tsv)
echo "SAP-linked SAS: $SAP_SAS"
# Test read access works
curl -s "https://$SA.blob.core.windows.net/logs/2024/jan/log1.txt?$SAP_SAS"
Revoke by deleting the Stored Access Policy:
# Delete the SAP — this immediately invalidates all SAS tokens linked to it
az storage container policy delete \
--account-name $SA \
--account-key $ACCT_KEY \
--container-name "logs" \
--name "read-policy"
# Test: the SAP-linked SAS should now return 403
curl -I "https://$SA.blob.core.windows.net/logs/2024/jan/log1.txt?$SAP_SAS"
# Expected: 403 Forbidden — AuthorizationPermissionMismatch or similar
This is the key exam distinction: SAP-linked SAS → revoke by deleting the SAP. Standalone SAS → can only invalidate by rotating the account key (affects ALL SAS signed with that key).
Step 7: Enable Soft Delete (Bonus)
# Enable blob soft delete with 7-day retention
az storage account blob-service-properties update \
--account-name $SA \
--resource-group $RG \
--enable-delete-retention true \
--delete-retention-days 7
# Delete a blob
az storage blob delete \
--account-name $SA \
--account-key $ACCT_KEY \
--container-name "logs" \
--name "2024/jan/log1.txt"
# List deleted blobs (only visible with --include d)
az storage blob list \
--account-name $SA \
--account-key $ACCT_KEY \
--container-name "logs" \
--include d \
--query "[?deleted==\`true\`].{name:name, deleted:deleted}" \
-o table
# Undelete (restore)
az storage blob undelete \
--account-name $SA \
--account-key $ACCT_KEY \
--container-name "logs" \
--name "2024/jan/log1.txt"
Step 8: Clean Up
az group delete --name $RG --yes --no-wait
rm -f log1.txt log2.txt archive.txt lifecycle-policy.json
Lab Tricky Spots Summary
| Trap | Effect | Fix |
|---|---|---|
| Setting a blob to Archive immediately | Blob becomes unreadable until rehydrated (up to 15h) | Use Cool instead for data you need to access again soon |
| Standalone SAS is hard to revoke | Must rotate account key, which breaks all SAS signed with it | Always use SAP-linked SAS for any long-lived or shared token |
--permissions string typos | Silent wrong access — too many or too few permissions granted | Test SAS access immediately after generating |
| Lifecycle policy not applying | Policy runs once daily, not instantly | In the exam, you're tested on configuration, not on observing live results |
Standard_GRS account cannot use ZRS blobs | Replication type is set at account creation | Choose the right SKU at account creation — you cannot change LRS→ZRS in-place on existing accounts |
Lab Takeaways
- Account key = total access — always prefer SAS or Managed Identity over keys in production.
- SAP-linked SAS is the only revocable SAS without rotating the account key.
- Archive tier makes blobs offline — model your data before archiving.
- Lifecycle policies are declarative — write the policy, let Azure execute it daily.
- Soft delete + versioning provide a safety net but add storage costs — enable for production, disable for temporary lab accounts.