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

Created 21 Oct 2026 · Modified 23 Oct 2026 · 1 version

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:

SettingHereChange when
skuStandard_ZRSYou need geo-redundancy (Standard_GZRS), or the region lacks ZRS (az storage sku list)
publicNetworkAccess + networkAclsEnabled, deny by default, IP listCallers are in Azure: add virtualNetworkRules or use Disabled with private endpoints (reference/storage/network-and-firewall.md)
Role scopeThe containerOne identity needs several containers: scope it to the account
principalTypeServicePrincipalFor a person, User; a group, Group. Setting it avoids PrincipalNotFound for a just-created identity (reference/rbac/errors.md)
Soft delete7 daysYour recovery window
isHnsEnabledomitted (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.

Read this document as markdown — the same bytes an agent receives.