Storage accounts often hold sensitive data - backups, logs, application data. But default settings prioritise convenience over security. Here's how to harden them.
The Security Checklist
| Setting | Recommended | Why |
|---|---|---|
| HTTPS only | Required | Prevent eavesdropping |
| Minimum TLS | 1.2 | Disable weak protocols |
| Public blob access | Disabled | Prevent data exposure |
| Shared key access | Disabled (if possible) | Use Azure AD instead |
| Soft delete (blobs) | Enabled, 14+ days | Recovery from accidents |
| Soft delete (containers) | Enabled, 14+ days | Recovery from accidents |
| Versioning | Enabled | Track changes |
| Network rules | Deny by default | Limit access |
Secure Terraform Configuration
resource "azurerm_storage_account" "secure" {
name = "stsecureexample"
resource_group_name = azurerm_resource_group.this.name
location = azurerm_resource_group.this.location
account_tier = "Standard"
account_replication_type = "GRS"
# Security settings
min_tls_version = "TLS1_2"
https_traffic_only_enabled = true
allow_nested_items_to_be_public = false
shared_access_key_enabled = false # Disable shared keys
# Blob properties
blob_properties {
versioning_enabled = true
delete_retention_policy {
days = 30
}
container_delete_retention_policy {
days = 30
}
}
# Network rules - deny by default
network_rules {
default_action = "Deny"
bypass = ["AzureServices"]
virtual_network_subnet_ids = [azurerm_subnet.app.id]
ip_rules = ["203.0.113.0/24"] # Office IPs
}
# Identity for Azure AD auth
identity {
type = "SystemAssigned"
}
}
Shared Key vs Azure AD Authentication
Shared keys (storage account key) are like root passwords - full access to everything. Azure AD authentication provides:
- Per-user access control
- Audit logging of who accessed what
- Conditional Access policies
- No long-lived secrets to rotate
Disable shared keys when possible:
shared_access_key_enabled = false
Then grant access via RBAC:
resource "azurerm_role_assignment" "blob_reader" {
scope = azurerm_storage_account.secure.id
role_definition_name = "Storage Blob Data Reader"
principal_id = data.azuread_group.readers.object_id
}
Network Isolation
Private Endpoints
For sensitive data, use private endpoints:
resource "azurerm_private_endpoint" "blob" {
name = "pe-storage-blob"
location = azurerm_resource_group.this.location
resource_group_name = azurerm_resource_group.this.name
subnet_id = azurerm_subnet.private_endpoints.id
private_service_connection {
name = "psc-blob"
private_connection_resource_id = azurerm_storage_account.secure.id
subresource_names = ["blob"]
is_manual_connection = false
}
}
Service Endpoints (Alternative)
Simpler but less secure than private endpoints:
resource "azurerm_subnet" "app" {
name = "app-subnet"
resource_group_name = azurerm_resource_group.this.name
virtual_network_name = azurerm_virtual_network.this.name
address_prefixes = ["10.0.1.0/24"]
service_endpoints = ["Microsoft.Storage"]
}
Trusted Microsoft Services
Some Azure services need access even with network rules:
network_rules {
default_action = "Deny"
bypass = ["AzureServices"] # Allows Azure Backup, Defender, etc.
}
Services covered by this bypass:
- Azure Backup
- Azure Defender for Storage
- Azure Event Grid
- Azure Log Analytics
- Azure Monitor
Soft Delete and Recovery
Enable recovery options:
blob_properties {
# Blob soft delete
delete_retention_policy {
days = 30
}
# Container soft delete
container_delete_retention_policy {
days = 30
}
# Versioning for accidental overwrites
versioning_enabled = true
}
# Point-in-time restore (optional, for critical data)
blob_properties {
restore_policy {
days = 7
}
change_feed_enabled = true # Required for restore
}
Encryption Options
Microsoft-Managed Keys (Default)
Enabled automatically, no configuration needed.
Customer-Managed Keys (CMK)
For regulatory requirements:
resource "azurerm_storage_account_customer_managed_key" "this" {
storage_account_id = azurerm_storage_account.secure.id
key_vault_id = azurerm_key_vault.this.id
key_name = azurerm_key_vault_key.storage.name
}
Infrastructure Encryption (Double Encryption)
Additional encryption layer:
resource "azurerm_storage_account" "double_encrypted" {
# ... other config ...
infrastructure_encryption_enabled = true
}
Monitoring and Alerting
Enable diagnostic logging:
resource "azurerm_monitor_diagnostic_setting" "storage" {
name = "storage-diagnostics"
target_resource_id = "${azurerm_storage_account.secure.id}/blobServices/default"
log_analytics_workspace_id = azurerm_log_analytics_workspace.this.id
enabled_log {
category = "StorageRead"
}
enabled_log {
category = "StorageWrite"
}
enabled_log {
category = "StorageDelete"
}
metric {
category = "Transaction"
enabled = true
}
}
Need help securing your Azure storage? Get in touch - we help organisations implement security best practices.