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 login complete
  • az storage extension available (bundled with core CLI)
az account show --query "{name:name, id:id}" -o table

Lab Objectives

  1. Create a GPv2 storage account with specific redundancy and access tier
  2. Upload blobs and change access tiers manually
  3. Configure a lifecycle management policy for automatic tiering
  4. Generate a Service SAS token with limited permissions
  5. Create a Stored Access Policy and link a SAS to it
  6. Revoke the SAP-linked SAS without touching the account key
  7. 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 true and --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 daysAfterModificationGreaterThan logic 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

TrapEffectFix
Setting a blob to Archive immediatelyBlob becomes unreadable until rehydrated (up to 15h)Use Cool instead for data you need to access again soon
Standalone SAS is hard to revokeMust rotate account key, which breaks all SAS signed with itAlways use SAP-linked SAS for any long-lived or shared token
--permissions string typosSilent wrong access — too many or too few permissions grantedTest SAS access immediately after generating
Lifecycle policy not applyingPolicy runs once daily, not instantlyIn the exam, you're tested on configuration, not on observing live results
Standard_GRS account cannot use ZRS blobsReplication type is set at account creationChoose the right SKU at account creation — you cannot change LRS→ZRS in-place on existing accounts

Lab Takeaways

  1. Account key = total access — always prefer SAS or Managed Identity over keys in production.
  2. SAP-linked SAS is the only revocable SAS without rotating the account key.
  3. Archive tier makes blobs offline — model your data before archiving.
  4. Lifecycle policies are declarative — write the policy, let Azure execute it daily.
  5. Soft delete + versioning provide a safety net but add storage costs — enable for production, disable for temporary lab accounts.