Before you start
- Sign in and select the right subscription:
az loginandaz account set --subscription "SUBSCRIPTION_NAME" jqmust be installed locally — the CSV export in Step 2 pipes JSON through it.- Commands are written for a POSIX shell (bash/zsh). On Windows, run them in WSL or Git Bash, or adapt the line continuations for PowerShell.
- Run each step from the same working directory so Step 3 can find the CSV that Step 2 writes.
Find publicly reachable accounts that are locked down by default
Lists every storage account whose public network access is Enabled while its network rule set's default action is Deny — i.e. reachable in principle, but only via explicit allow rules. Printed as a table for a quick visual check.
az storage account list \
--query "[?publicNetworkAccess=='Enabled' && networkRuleSet.defaultAction=='Deny'].{StorageAccountName:name, ResourceGroup:resourceGroup, Location:location, PublicNetworkAccess:publicNetworkAccess, DefaultAction:networkRuleSet.defaultAction}" \
--output tableExpect: a table with columns StorageAccountName, ResourceGroup, Location, PublicNetworkAccess, DefaultAction. An empty table means no accounts currently match the filter.
Export the matching accounts to a local CSV
Re-runs the same filter, keeps just the two fields you need for the next step, and pipes the JSON through jq to write a CSV to your current directory.
az storage account list \
--query "[?publicNetworkAccess=='Enabled' && networkRuleSet.defaultAction=='Deny'].{ResourceGroup:resourceGroup,StorageAccountName:name}" \
--output json | jq -r '["ResourceGroupName","StorageAccountName"], (.[] | [.ResourceGroup,.StorageAccountName]) | @csv' > selected-network-storageaccounts.csvExpect: a two-column CSV — ResourceGroupName,StorageAccountName — with one header row plus one row per matching account.
Add the two new public IPs to each account's allow list
Reads the CSV from Step 2 and, for every listed account, adds each of the two new public IP addresses as an allowed network rule — no need to touch accounts by hand.
defaultAction — traffic from anywhere else is still denied.# The two new public IPs to allow through the storage firewall
NEW_IPS=("203.0.113.10" "203.0.113.20")
# Skip the header row, strip quotes, loop over ResourceGroup,StorageAccountName
while IFS=, read -r ResourceGroup StorageAccountName; do
for ip in "${NEW_IPS[@]}"; do
az storage account network-rule add \
--resource-group "$ResourceGroup" \
--account-name "$StorageAccountName" \
--ip-address "$ip"
done
done < <(tail -n +2 selected-network-storageaccounts.csv | tr -d '"')Expect: one JSON confirmation per network-rule add call — two per storage account. Verify with az storage account network-rule list --account-name <name> --resource-group <rg>.
Full script: allow the IPs and lock to selected networks
A standalone .sh version of Step 3 with progress logging built in. It also explicitly sets --default-action Deny with --bypass AzureServices on each account, so the firewall stays closed to everything except the IPs you just added and Azure's own services.
--default-action Deny on every account it touches — confirm that's the desired end state, not just an allow-list addition.#!/bin/bash
CSV_FILE="$HOME/storageaccountscopy.csv" # set to your CSV path
ALLOWED_IPS=(
"203.0.113.10"
"203.0.113.20"
)
tail -n +2 "$CSV_FILE" | while IFS=, read -r ResourceGroupName StorageAccountName
do
ResourceGroupName=$(echo "$ResourceGroupName" | xargs)
StorageAccountName=$(echo "$StorageAccountName" | xargs)
echo "Updating storage account: $StorageAccountName in resource group: $ResourceGroupName"
for IP in "${ALLOWED_IPS[@]}"
do
echo " Adding IP rule: $IP"
az storage account network-rule add \
--resource-group "$ResourceGroupName" \
--account-name "$StorageAccountName" \
--ip-address "$IP"
done
echo " Setting default action to Deny / selected networks"
az storage account update \
--resource-group "$ResourceGroupName" \
--name "$StorageAccountName" \
--default-action Deny \
--bypass AzureServices
echo "Done with $StorageAccountName"
echo "--------------------------------------"
doneExpect: console output logging each account as it's processed, two IP-rule confirmations and one az storage account update confirmation per account, ending in a divider line.