Back to blog
AzureBest PracticesCloud SecurityNetworkingStorage

Azure Storage Account Allows Public Network Access: Why It Matters and How to Fix It

Learn why an Azure Storage account with default network action set to Allow is risky, plus CLI, Terraform, and Azure Policy steps to lock it down.

TL;DR

This check flags Azure Storage accounts whose default network rule is set to Allow, meaning any client on the public internet can reach the storage endpoint. Flip the default action to Deny and add explicit firewall rules or private endpoints so only trusted networks can connect.

Azure Storage accounts are one of the most common places sensitive data ends up: application backups, log archives, customer uploads, Terraform state, and the occasional credentials file someone swore they would clean up later. By default, a storage account exposes its blob, file, queue, and table endpoints over the public internet. Access is still gated by keys, SAS tokens, and Azure AD, but the network surface is wide open to anyone who can resolve the endpoint name.

The Storage Account Allows Public Network Access check looks at one specific setting: the storage account's default network access action. When that action is Allow, the account accepts traffic from any source IP. This post explains what that setting does, why an open network surface matters even with authentication in place, and how to lock it down without breaking your applications.


What this check detects

Every Azure Storage account has a network ruleset with a property called defaultAction. It controls what happens to a request that does not match any explicit virtual network rule or IP rule:

  • Allow — requests from any network are accepted at the network layer. This is the default when you create a storage account without specifying network rules.
  • Deny — requests are rejected unless they match an allowed virtual network, an allowed IP range, a configured private endpoint, or a trusted Azure service exception.

The Lensix check storage_publicnetworkaccess reports a finding when networkRuleSet.defaultAction is set to Allow. You can confirm the current state on any account with the CLI:

az storage account show \
  --name mystorageacct \
  --resource-group my-rg \
  --query "networkRuleSet.defaultAction" \
  --output tsv

If that returns Allow, the account is in scope for this check.

Note: There is a separate, account-level publicNetworkAccess property that can be set to Disabled to turn off public access entirely (used with private endpoints). The defaultAction setting is the more granular firewall control. This check focuses on defaultAction, but both work together, and we cover the private endpoint path below.


Why it matters

The common pushback is: "Storage requests still need a valid key or SAS token, so why does the network matter?" Authentication is the last line of defense, not the only one. Leaving the network open removes a layer that would otherwise stop a whole class of attacks before they reach the auth check.

Leaked keys become instantly exploitable

Storage account keys and SAS tokens leak constantly: committed to Git, baked into mobile apps, pasted into support tickets, captured in CI logs. If the storage firewall is open, a leaked key works from anywhere on the planet the moment it is exposed. If the firewall only allows traffic from your VNet, that same leaked key is useless to an attacker sitting outside your network.

Warning: Account keys grant full control over the entire storage account, including the ability to read and delete all containers. A network restriction is often the difference between a leaked key being a non-event and being a full data breach.

Data exfiltration and ransomware

Publicly reachable accounts are a favorite target for automated scanning. Attackers enumerate storage endpoints, test for anonymous container access, and probe leaked credentials. Once inside, they can exfiltrate data, delete blobs, or encrypt content and demand payment. A closed network surface dramatically shrinks the window for all of this.

Compliance and audit findings

Frameworks such as CIS Azure Foundations, PCI DSS, and HIPAA expect storage that holds regulated data to restrict network access. An account with defaultAction: Allow will show up as a finding in nearly every audit, and "it requires a key" is rarely an accepted justification for a system holding sensitive data.

Defense in depth means an attacker has to beat more than one control. Open network access collapses your storage security down to a single secret, and secrets leak.


How to fix it

The fix is to set defaultAction to Deny and then explicitly allow the networks that legitimately need access. Do the allow rules first, or you risk locking yourself and your apps out the instant you flip the default.

Danger: Setting defaultAction to Deny immediately blocks every source that is not explicitly allowed, including the machine running the command, App Services, Functions, and any analytics jobs reading the account. Map out who connects before you change this on a production account.

Step 1: Identify legitimate traffic sources

Before flipping the switch, build an inventory:

  • Which VNets and subnets host apps that read or write the account?
  • Which office or VPN egress IPs do administrators connect from?
  • Do any Azure services (Backup, Monitor, Event Grid) need access?

You can enable logging first to capture the actual source IPs hitting the account, which helps avoid surprises:

# Check existing network rules before changing anything
az storage account network-rule list \
  --account-name mystorageacct \
  --resource-group my-rg \
  --output table

Step 2: Add the allow rules

Allow a specific subnet (the service endpoint for Microsoft.Storage must be enabled on the subnet first):

# Enable the storage service endpoint on the subnet
az network vnet subnet update \
  --resource-group my-rg \
  --vnet-name my-vnet \
  --name app-subnet \
  --service-endpoints Microsoft.Storage

# Allow that subnet on the storage account
az storage account network-rule add \
  --account-name mystorageacct \
  --resource-group my-rg \
  --vnet-name my-vnet \
  --subnet app-subnet

Allow a specific public IP range (for example, your office egress):

az storage account network-rule add \
  --account-name mystorageacct \
  --resource-group my-rg \
  --ip-address 203.0.113.0/24

Step 3: Set the default action to Deny

az storage account update \
  --name mystorageacct \
  --resource-group my-rg \
  --default-action Deny \
  --bypass AzureServices

The --bypass AzureServices flag allows trusted Microsoft services (such as Azure Backup and Monitor) that are granted the right role to reach the account even with the firewall on. Drop it to None if you want zero exceptions.

Step 4: Verify

az storage account show \
  --name mystorageacct \
  --resource-group my-rg \
  --query "networkRuleSet.{default:defaultAction, bypass:bypass, ipRules:ipRules, vnetRules:virtualNetworkRules}" \
  --output json

Going further: private endpoints

For the strongest posture, disable public access entirely and use a private endpoint so the account is only reachable over a private IP inside your VNet:

# Turn off all public network access
az storage account update \
  --name mystorageacct \
  --resource-group my-rg \
  --public-network-access Disabled

# Create a private endpoint into your subnet
az network private-endpoint create \
  --name mystorageacct-pe \
  --resource-group my-rg \
  --vnet-name my-vnet \
  --subnet app-subnet \
  --private-connection-resource-id $(az storage account show -n mystorageacct -g my-rg --query id -o tsv) \
  --group-id blob \
  --connection-name mystorageacct-conn

Note: Private endpoints require a Private DNS zone (privatelink.blob.core.windows.net) so that clients resolve the account name to the private IP. Without DNS integration, your apps will still try to reach the public endpoint and fail.

Terraform example

resource "azurerm_storage_account" "secure" {
  name                     = "mystorageacct"
  resource_group_name      = azurerm_resource_group.rg.name
  location                 = azurerm_resource_group.rg.location
  account_tier             = "Standard"
  account_replication_type = "LRS"

  network_rules {
    default_action = "Deny"
    bypass         = ["AzureServices"]
    ip_rules       = ["203.0.113.0/24"]
    virtual_network_subnet_ids = [
      azurerm_subnet.app.id,
    ]
  }
}

Tip: Define the firewall rules inside the storage account resource block as shown, rather than as separate azurerm_storage_account_network_rules resources. Mixing both styles causes Terraform to fight itself and produce a perpetual diff.


How to prevent it from happening again

Fixing one account is easy. Keeping every new account closed by default is the real work, and it belongs in policy and pipelines, not in a human's memory.

Azure Policy

Use a built-in or custom policy to deny creation of storage accounts that allow public network access. The effect can be Deny to block them outright or Audit to start by flagging them:

{
  "if": {
    "allOf": [
      {
        "field": "type",
        "equals": "Microsoft.Storage/storageAccounts"
      },
      {
        "field": "Microsoft.Storage/storageAccounts/networkAcls.defaultAction",
        "equals": "Allow"
      }
    ]
  },
  "then": {
    "effect": "deny"
  }
}

Assign it at the management group or subscription scope so it covers every team, not just the ones who remember to opt in.

Warning: Roll out a Deny policy in Audit mode first. A hard deny applied broadly can break deployment pipelines that create accounts with default settings before configuring rules. Watch the compliance results for a week, fix the noisy templates, then switch to deny.

CI/CD gates

Scan infrastructure code before it ships. Tools like Checkov, tfsec, and Terrascan all catch open storage firewalls. A sample Checkov gate in a pipeline:

checkov -d ./infra \
  --framework terraform \
  --check CKV_AZURE_35   # Ensure default network access rule is set to deny

Fail the build when this check fails, and the misconfiguration never reaches Azure in the first place.

Tip: Pair pre-deployment scanning with continuous detection in Lensix. IaC scanning catches what you build through pipelines; continuous scanning catches the accounts created by hand in the portal during an incident and never cleaned up.


Best practices

  • Deny by default, allow by exception. Every storage account should start with defaultAction: Deny and add only the networks that need access.
  • Prefer private endpoints for sensitive data. IP and VNet rules are good; a private endpoint with public access disabled is better and removes the public surface entirely.
  • Avoid account keys where you can. Use Azure AD (Microsoft Entra ID) authentication and managed identities instead of shared keys, and disable shared key access on accounts that do not need it.
  • Scope SAS tokens tightly. Short expiry, least privilege, and IP restrictions on any token you must issue.
  • Enable logging. Turn on diagnostic settings and storage analytics so you can see who is connecting and from where, which also makes tightening rules far safer.
  • Review the bypass list. AzureServices is convenient but it does allow a broad set of Microsoft services. Use None for the most sensitive accounts and explicitly grant access where required.

Open network access on a storage account is one of those settings that feels harmless because authentication is still in place. In practice it removes a layer of defense exactly where you most want one, in front of the data. Set defaultAction to Deny, allow only the networks that need in, and enforce it with policy so the next account is locked down without anyone having to think about it.