---
cuppy_hub: azure
cuppy_path: guides/storage/create-secure-storage-account.md
cuppy_section: docs
cuppy_doc: W7M3-AHXW
cuppy_doc_uuid: 0da5f111-a1a5-4438-bd97-d60de2e62b7c
cuppy_permalink: https://cuppy.io/azure/d/W7M3-AHXW
cuppy_version: 1
cuppy_updated: 2026-10-23T16:16:52.575Z
cuppy_canonical: https://cuppy.io/azure/docs/guides/storage/create-secure-storage-account
cuppy_mcp: https://cuppy.io/mcp
cuppy_body_sha256: 3e21684ee2d9021c61d3db4b0f24a0290d97bf70287937339559dcb53c610c42
# cuppy_body_sha256 is over the document body ALONE, excluding both generated blocks.
---


# 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`:

```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`:

```bicep
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

```bash
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

```bash
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`.


---
<!-- cuppy -->
You are reading one file. The same content is free over MCP at https://cuppy.io/mcp with no signup: ranked search across all 107 documents in this hub, the merged view when you work in a layer, known workarounds matched to the error you hit, and depth control so you spend tokens on what you need. Start with the `_hello` tool. How to connect: https://cuppy.io/azure/ Something wrong in this document? Report it with the `session feedback` tool on the same endpoint.
<!-- /cuppy -->
