Skip to content

Azure Storage Security & Guardrails

This guide covers essential security configurations, guardrails, and request limiting strategies to protect your Azure Blob Storage and control access to your image assets.

Azure Storage provides multiple layers of access control:

  • Account Level: Storage account access keys and SAS
  • Container Level: Container permissions and access policies
  • Blob Level: Individual blob permissions
  • Network Level: Firewall rules and virtual networks
  1. Storage Account Keys (Shared Key)
  2. Shared Access Signatures (SAS)
  3. Azure Active Directory (Azure AD)
  4. Anonymous Public Access (Limited scenarios)
Section titled “2.1 Disable Storage Account Keys (Recommended)”
Terminal window
# Disable shared key access for enhanced security
az storage account update \
--name $STORAGE_ACCOUNT \
--resource-group $RESOURCE_GROUP \
--allow-shared-key-access false
# Enable Azure AD authentication only
az storage account update \
--name $STORAGE_ACCOUNT \
--resource-group $RESOURCE_GROUP \
--enable-azure-ad-ds false
Terminal window
# Enforce TLS 1.2 minimum
az storage account update \
--name $STORAGE_ACCOUNT \
--resource-group $RESOURCE_GROUP \
--min-tls-version TLS1_2 \
--https-only true
Terminal window
# Enable soft delete for containers (up to 365 days)
az storage account blob-service-properties update \
--account-name $STORAGE_ACCOUNT \
--resource-group $RESOURCE_GROUP \
--enable-container-delete-retention true \
--container-delete-retention-days 7
# Enable soft delete for blobs
az storage account blob-service-properties update \
--account-name $STORAGE_ACCOUNT \
--resource-group $RESOURCE_GROUP \
--enable-delete-retention true \
--delete-retention-days 30

🔒 Shared Access Signatures (SAS) Best Practices

Section titled “🔒 Shared Access Signatures (SAS) Best Practices”

3.1 Generate Account-Level SAS with Restrictions

Section titled “3.1 Generate Account-Level SAS with Restrictions”
Terminal window
# Generate restrictive account SAS
az storage account generate-sas \
--account-name $STORAGE_ACCOUNT \
--account-key $STORAGE_KEY \
--services b \
--resource-types sco \
--permissions r \
--expiry "2024-12-31T23:59:59Z" \
--https-only \
--start "2024-01-01T00:00:00Z"

3.2 Container-Level SAS for Specific Access

Section titled “3.2 Container-Level SAS for Specific Access”
Terminal window
# Generate container SAS with read-only access
az storage container generate-sas \
--account-name $STORAGE_ACCOUNT \
--account-key $STORAGE_KEY \
--name images \
--permissions r \
--expiry "2024-06-30T23:59:59Z" \
--https-only \
--ip "203.0.113.0/24"
Terminal window
# Generate blob-specific SAS
az storage blob generate-sas \
--account-name $STORAGE_ACCOUNT \
--account-key $STORAGE_KEY \
--container-name images \
--name "private-document.pdf" \
--permissions r \
--expiry "2024-03-15T23:59:59Z" \
--https-only

🔥 Network Security & Firewall Configuration

Section titled “🔥 Network Security & Firewall Configuration”
Terminal window
# Set default action to deny
az storage account update \
--name $STORAGE_ACCOUNT \
--resource-group $RESOURCE_GROUP \
--default-action Deny
# Allow specific IP ranges (your office, CI/CD systems)
az storage account network-rule add \
--account-name $STORAGE_ACCOUNT \
--resource-group $RESOURCE_GROUP \
--ip-address "203.0.113.0/24"
# Allow Azure services
az storage account update \
--name $STORAGE_ACCOUNT \
--resource-group $RESOURCE_GROUP \
--bypass AzureServices
Terminal window
# Create virtual network and subnet
az network vnet create \
--resource-group $RESOURCE_GROUP \
--name MyVNet \
--address-prefix 10.0.0.0/16 \
--subnet-name MySubnet \
--subnet-prefix 10.0.1.0/24
# Enable service endpoint for storage
az network vnet subnet update \
--resource-group $RESOURCE_GROUP \
--vnet-name MyVNet \
--name MySubnet \
--service-endpoints Microsoft.Storage
# Add VNet rule to storage account
az storage account network-rule add \
--account-name $STORAGE_ACCOUNT \
--resource-group $RESOURCE_GROUP \
--vnet MyVNet \
--subnet MySubnet
Terminal window
# Create private endpoint
az network private-endpoint create \
--resource-group $RESOURCE_GROUP \
--name storage-private-endpoint \
--vnet-name MyVNet \
--subnet MySubnet \
--private-connection-resource-id "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Storage/storageAccounts/$STORAGE_ACCOUNT" \
--group-ids blob \
--connection-name storage-connection
# Create DNS zone for private endpoint
az network private-dns zone create \
--resource-group $RESOURCE_GROUP \
--name "privatelink.blob.core.windows.net"
# Link DNS zone to VNet
az network private-dns link vnet create \
--resource-group $RESOURCE_GROUP \
--zone-name "privatelink.blob.core.windows.net" \
--name MyDNSLink \
--virtual-network MyVNet \
--registration-enabled false

Azure Storage has built-in limits per account:

  • Request rate: Up to 20,000 requests/second for hot storage
  • Ingress: Up to 25 Gbps (US regions)
  • Egress: Up to 50 Gbps (US regions)
  • IOPS: Up to 20,000 (for premium storage)

5.2 Implement Application-Level Rate Limiting

Section titled “5.2 Implement Application-Level Rate Limiting”

Node.js with Express Rate Limiting:

const rateLimit = require('express-rate-limit');
const { BlobServiceClient } = require('@azure/storage-blob');
// Rate limiting middleware
const imageRequestLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: 'Too many image requests from this IP, please try again later.',
standardHeaders: true,
legacyHeaders: false,
});
// Apply rate limiting to image routes
app.use('/api/images', imageRequestLimiter);
// Track and limit per-user requests
const userRequestTracker = new Map();
app.get('/api/images/:imageName', async (req, res) => {
const clientIP = req.ip;
const userId = req.headers['x-user-id'] || clientIP;
// Check daily limit per user
const today = new Date().toDateString();
const key = `${userId}-${today}`;
if (!userRequestTracker.has(key)) {
userRequestTracker.set(key, 0);
}
const currentRequests = userRequestTracker.get(key);
if (currentRequests >= 1000) { // Daily limit of 1000 requests per user
return res.status(429).json({
error: 'Daily request limit exceeded',
limit: 1000,
resetTime: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString()
});
}
userRequestTracker.set(key, currentRequests + 1);
try {
// Generate SAS token for the image
const sasToken = generateImageSAS(req.params.imageName);
res.json({ imageUrl: `${baseImageUrl}/${req.params.imageName}?${sasToken}` });
} catch (error) {
res.status(500).json({ error: 'Failed to generate image URL' });
}
});

Configure rate limiting rules in Azure CDN:

Terminal window
# Create CDN profile with rate limiting capabilities
az cdn profile create \
--resource-group $RESOURCE_GROUP \
--name cdn-with-rate-limits \
--sku Premium_Verizon # Required for advanced rules
# The rate limiting rules would be configured through Azure Portal
# under CDN Profile > Rules Engine

CDN Rate Limiting Rule Example:

{
"name": "RateLimitRule",
"order": 1,
"conditions": [
{
"name": "RequestRate",
"parameters": {
"requestRate": "100",
"duration": "60",
"operator": "GreaterThan"
}
}
],
"actions": [
{
"name": "CacheExpiration",
"parameters": {
"cacheBehavior": "Override",
"cacheType": "All",
"cacheDuration": "00:05:00"
}
}
]
}
Terminal window
# Enable storage analytics logging
az storage logging update \
--account-name $STORAGE_ACCOUNT \
--account-key $STORAGE_KEY \
--services b \
--log rwd \
--retention 90
# Enable metrics
az storage metrics update \
--account-name $STORAGE_ACCOUNT \
--account-key $STORAGE_KEY \
--services b \
--api true \
--hour true \
--minute false \
--retention 90
Terminal window
# Create action group for security alerts
az monitor action-group create \
--resource-group $RESOURCE_GROUP \
--name "security-alerts" \
--action email security admin@yourdomain.com \
--action webhook https://your-security-webhook.com/alert
# Alert for unusual access patterns
az monitor metrics alert create \
--name "High Blob Request Rate" \
--resource-group $RESOURCE_GROUP \
--scopes "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Storage/storageAccounts/$STORAGE_ACCOUNT" \
--condition "avg Transactions > 10000" \
--action security-alerts \
--description "Alert when blob requests exceed normal patterns"
# Alert for unauthorized access attempts
az monitor metrics alert create \
--name "Unauthorized Access" \
--resource-group $RESOURCE_GROUP \
--scopes "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Storage/storageAccounts/$STORAGE_ACCOUNT" \
--condition "total ClientOtherError > 50" \
--action security-alerts \
--description "Alert on multiple client authentication errors"
Terminal window
# Create budget for storage account
az consumption budget create \
--resource-group $RESOURCE_GROUP \
--budget-name "storage-monthly-budget" \
--amount 100 \
--time-grain Monthly \
--start-date "2024-01-01" \
--notifications '[
{
"enabled": true,
"operator": "GreaterThan",
"threshold": 80,
"contactEmails": ["admin@yourdomain.com"]
},
{
"enabled": true,
"operator": "GreaterThan",
"threshold": 100,
"contactEmails": ["admin@yourdomain.com"]
}
]'

7.2 Implement Usage-Based Pricing Protection

Section titled “7.2 Implement Usage-Based Pricing Protection”
// Cost tracking middleware
const costTracker = {
// Estimate cost per operation
calculateRequestCost(operation, count = 1) {
const costs = {
'read': 0.0004 / 10000, // $0.0004 per 10K read operations
'write': 0.05 / 10000, // $0.05 per 10K write operations
'list': 0.05 / 10000, // $0.05 per 10K list operations
'delete': 0.0004 / 10000 // $0.0004 per 10K delete operations
};
return (costs[operation] || 0) * count;
},
// Track daily costs per user
async trackUserCost(userId, operation, count = 1) {
const cost = this.calculateRequestCost(operation, count);
const today = new Date().toDateString();
const key = `cost-${userId}-${today}`;
// Store in your preferred database/cache
const currentCost = await redis.get(key) || 0;
const newCost = parseFloat(currentCost) + cost;
await redis.set(key, newCost, 'EX', 86400); // Expire after 24 hours
return newCost;
}
};
// Usage in API endpoints
app.get('/api/images/:imageName', async (req, res) => {
const userId = req.headers['x-user-id'] || req.ip;
// Track cost
const dailyCost = await costTracker.trackUserCost(userId, 'read');
// Implement cost limit (e.g., $1 per user per day)
if (dailyCost > 1.00) {
return res.status(429).json({
error: 'Daily cost limit exceeded',
currentCost: dailyCost,
limit: 1.00
});
}
// Continue with request...
});
Terminal window
# Enable Security Center for the subscription
az security auto-provisioning-setting update \
--name default \
--auto-provision on
# Enable threat detection for storage accounts
az security setting update \
--name "MCAS" \
--enabled true
// Security monitoring service
class SecurityMonitor {
constructor(storageAccount) {
this.storageAccount = storageAccount;
this.suspiciousPatterns = new Set();
}
// Monitor for suspicious access patterns
async analyzeRequest(req) {
const clientIP = req.ip;
const userAgent = req.headers['user-agent'];
const timestamp = new Date();
// Check for suspicious patterns
const patterns = {
rapidRequests: await this.checkRapidRequests(clientIP),
unusualUserAgent: this.checkUnusualUserAgent(userAgent),
geographicAnomaly: await this.checkGeographicAnomaly(clientIP),
timeAnomaly: this.checkTimeAnomaly(timestamp)
};
const suspiciousScore = Object.values(patterns)
.filter(Boolean).length;
if (suspiciousScore >= 2) {
await this.triggerSecurityAlert(clientIP, patterns);
return false; // Block request
}
return true; // Allow request
}
async checkRapidRequests(clientIP) {
const key = `requests-${clientIP}`;
const count = await redis.incr(key);
await redis.expire(key, 60); // 1 minute window
return count > 100; // More than 100 requests per minute
}
checkUnusualUserAgent(userAgent) {
const commonBots = [
'bot', 'crawler', 'spider', 'scraper',
'wget', 'curl', 'python-requests'
];
return commonBots.some(bot =>
userAgent?.toLowerCase().includes(bot)
);
}
async triggerSecurityAlert(clientIP, patterns) {
const alert = {
timestamp: new Date(),
clientIP,
patterns,
severity: 'HIGH',
action: 'BLOCKED'
};
// Log to security monitoring system
console.log('SECURITY ALERT:', JSON.stringify(alert));
// Send to security team
await this.notifySecurityTeam(alert);
// Temporarily block IP
await this.temporaryBlockIP(clientIP);
}
async temporaryBlockIP(clientIP) {
// Add to temporary blocklist for 1 hour
await redis.set(`blocked-${clientIP}`, '1', 'EX', 3600);
}
}
  • Disable Storage Account Keys (use Azure AD when possible)
  • Enable HTTPS-only access
  • Set minimum TLS version to 1.2
  • Configure firewall rules to restrict access
  • Enable soft delete for blobs and containers
  • Implement proper CORS settings
  • Use SAS tokens for controlled access
  • Enable logging and monitoring
  • Set up security alerts
  • Regular security reviews
  • Implement Private Endpoints
  • Use Customer-Managed Keys (CMK)
  • Enable Azure AD authentication
  • Implement network isolation
  • Use Azure Security Center recommendations
  • Regular penetration testing
  • Implement Zero Trust architecture
  • Application-level rate limiting
  • Per-user request quotas
  • CDN rate limiting rules
  • Cost monitoring and alerts
  • Usage-based access controls
  • Regular cost reviews
Terminal window
# Create Key Vault
az keyvault create \
--resource-group $RESOURCE_GROUP \
--name "mykeyvault$RANDOM" \
--location $LOCATION \
--enable-purge-protection true
# Create encryption key
az keyvault key create \
--vault-name "mykeyvault$RANDOM" \
--name "storage-encryption-key" \
--kty RSA \
--size 2048
# Configure storage account to use CMK
az storage account update \
--name $STORAGE_ACCOUNT \
--resource-group $RESOURCE_GROUP \
--encryption-key-source Microsoft.Keyvault \
--encryption-key-vault "https://mykeyvault$RANDOM.vault.azure.net/" \
--encryption-key-name "storage-encryption-key"
Terminal window
# Create container with immutable policy
az storage container create \
--name immutable-images \
--account-name $STORAGE_ACCOUNT \
--account-key $STORAGE_KEY
# Set immutability policy (for compliance requirements)
az storage container immutability-policy create \
--account-name $STORAGE_ACCOUNT \
--container-name immutable-images \
--period 365 \
--allow-protected-append-writes false

Immediate Response:

  1. Identify scope of the incident
  2. Temporarily block suspicious IPs
  3. Rotate storage account keys
  4. Review access logs
  5. Notify stakeholders

Investigation:

  1. Analyze logs for attack patterns
  2. Check for data exfiltration
  3. Identify compromised assets
  4. Document findings

Recovery:

  1. Remove malicious access
  2. Restore from backups if needed
  3. Update security configurations
  4. Monitor for additional activity
// Automated incident response
class IncidentResponse {
async handleSecurityIncident(incident) {
// Immediate actions
await this.blockSuspiciousIPs(incident.sourceIPs);
await this.rotateAccessKeys();
await this.notifySecurityTeam(incident);
// Generate incident report
const report = await this.generateIncidentReport(incident);
await this.logIncident(report);
return report;
}
async blockSuspiciousIPs(ips) {
for (const ip of ips) {
await this.addFirewallRule('DENY', ip);
}
}
async rotateAccessKeys() {
// Rotate storage account keys
await this.executeAzureCLI([
'az', 'storage', 'account', 'keys', 'renew',
'--account-name', this.storageAccount,
'--key', 'key1'
]);
}
}

For critical security incidents or compliance questions:

  • Azure Security Center recommendations
  • Microsoft Security Response Center (MSRC)
  • Azure support tickets for security issues
  • Regular security audits and compliance reviews