Create a storage account that is keyless, private to the right networks and redeploys cleanly
Create a StorageV2 account in Bicep (API 2026-06-01, the newest with Bicep types) or with the Azure CLI, with every security default written out — TLS 1.2, Shared Key off, portal defaulting to Entra ID, no anonymous access, copy scope limited to the tenant, firewall deny with explicit rules — plus blob and container soft delete, a container, a data role on the container, and the queries that prove the result; with what each setting breaks.
markdownguidev1W7M3-AHXWstage: buildtrust: officialservice: storageinterface: iacinterface: cliMicrosoft.Storage/storageAccounts@2026-06-01Bicep storage accountallowSharedKeyAccess false BicepminimumTlsVersion TLS1_2defaultToOAuthAuthentication trueallowBlobPublicAccess falseallowedCopyScope AADallowCrossTenantReplication falsenetworkAcls defaultAction Deny BicepblobServices deleteRetentionPolicycontainerDeleteRetentionPolicycontainers publicAccess NoneStorage Blob Data Contributor role assignment container scopeba92f5b4-2d11-453d-a403-e96b0029c9feaz storage account create secure--allow-shared-key-access false--min-tls-version TLS1_2storage BCP081 2026-09-01
Create a storage account that is keyless and firewalled
Write every security setting explicitly. Several defaults are permissive or depend on the API version and tool: Shared Key is allowed when unset, the spec still calls TLS 1.0 the default, and the CLI picks Standard_RAGRS (reference/storage/access-models.md). Checked with Bicep 0.48.1: storage 2026-09-01 is stable in the REST specs but has no Bicep types (BCP081, so no property checking), so this uses 2026-06-01.
1. Bicep
main.bicep:
@description('Globally unique: 3-24 lowercase letters and numbers.')
@minLength(3)
@maxLength(24)
param accountName string
param location string = resourceGroup().location
@description('Object ID of the identity that reads and writes blobs (for example a managed identity principalId).')
param dataPrincipalId string
@allowed(['ServicePrincipal', 'User', 'Group'])
param dataPrincipalType string = 'ServicePrincipal'
@description('Public IPv4 addresses or CIDR ranges allowed through the firewall. Same-region Azure callers need a VNet rule or private endpoint instead.')
param allowedIpRanges array = []
resource account 'Microsoft.Storage/storageAccounts@2026-06-01' = {
name: accountName
location: location
kind: 'StorageV2'
sku: {
name: 'Standard_ZRS'
}
properties: {
minimumTlsVersion: 'TLS1_2'
supportsHttpsTrafficOnly: true
allowSharedKeyAccess: false
defaultToOAuthAuthentication: true
allowBlobPublicAccess: false
allowCrossTenantReplication: false
allowedCopyScope: 'AAD'
publicNetworkAccess: 'Enabled'
networkAcls: {
defaultAction: 'Deny'
bypass: 'AzureServices'
ipRules: [for ip in allowedIpRanges: {
value: ip
action: 'Allow'
}]
}
}
}
resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2026-06-01' = {
parent: account
name: 'default'
properties: {
deleteRetentionPolicy: {
enabled: true
days: 7
}
containerDeleteRetentionPolicy: {
enabled: true
days: 7
}
}
}
resource container 'Microsoft.Storage/storageAccounts/blobServices/containers@2026-06-01' = {
parent: blobService
name: 'orders'
properties: {
publicAccess: 'None'
}
}
// Storage Blob Data Contributor
var blobDataContributor = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
resource dataRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
scope: container
name: guid(container.id, dataPrincipalId, blobDataContributor)
properties: {
roleDefinitionId: blobDataContributor
principalId: dataPrincipalId
principalType: dataPrincipalType
}
}
output blobEndpoint string = account.properties.primaryEndpoints.blob
output accountId string = account.id
main.bicepparam:
using './main.bicep'
param accountName = 'stordersprod001'
param dataPrincipalId = '00000000-0000-0000-0000-000000000000'
param allowedIpRanges = ['203.0.113.0/24']
Deploy: az deployment group create -g <rg> -f main.bicep -p main.bicepparam (preview first with what-if; guides/bicep/deploy-bicep.md). The containers and role assignment go through Resource Manager, so the deployment works with Shared Key off.
Choices to make, not copy:
| Setting | Here | Change when |
|---|---|---|
sku | Standard_ZRS | You need geo-redundancy (Standard_GZRS), or the region lacks ZRS (az storage sku list) |
publicNetworkAccess + networkAcls | Enabled, deny by default, IP list | Callers are in Azure: add virtualNetworkRules or use Disabled with private endpoints (reference/storage/network-and-firewall.md) |
| Role scope | The container | One identity needs several containers: scope it to the account |
principalType | ServicePrincipal | For a person, User; a group, Group. Setting it avoids PrincipalNotFound for a just-created identity (reference/rbac/errors.md) |
| Soft delete | 7 days | Your recovery window |
isHnsEnabled | omitted (false) | You need directories and ACLs: set true now, since enabling it later is a one-way upgrade |
What these settings break, so you can warn your human: Shared Key off blocks connection strings, account and service SAS, the portal's Azure Files view, Cloud Shell file shares, and any tool not using Entra ID. Copy scope AAD blocks copies from accounts in other tenants. The firewall blocks the portal's data views from outside the allowed networks.
2. Azure CLI equivalent
az storage account create -n <acct> -g <rg> -l <region> --kind StorageV2 --sku Standard_ZRS \
--min-tls-version TLS1_2 --https-only true --allow-shared-key-access false \
--allow-blob-public-access false --allow-cross-tenant-replication false --allowed-copy-scope AAD \
--default-action Deny --bypass AzureServices
az storage account network-rule add -n <acct> -g <rg> --ip-address <public-ip-or-cidr>
az storage container create -n orders --account-name <acct> --auth-mode login
CLI 2.91 has no flag for defaultToOAuthAuthentication: set it in Bicep, or with the portal's Default to Microsoft Entra authorization in the Azure portal. az storage container create … --auth-mode login is a data-plane call: it needs a data role and your IP in the firewall. Behind a firewall, create containers in Bicep instead.
3. Prove it
az storage account show -n <acct> -g <rg> --query "{tls:minimumTlsVersion, sharedKey:allowSharedKeyAccess, oauth:defaultToOAuthAuthentication, anon:allowBlobPublicAccess, copy:allowedCopyScope, public:publicNetworkAccess, firewall:networkRuleSet.defaultAction, sku:sku.name}"
az role assignment list --scope <container-resource-id> -o table
Expect TLS1_2, false, true, false, AAD, Enabled, Deny. Then read and write with the identity: guides/storage/access-blobs-from-typescript.md.