I’m maintaining Azure Storage Account firewall rules using PowerShell. The script compares the latest Azure Service Tag JSON with the existing IP rules on a Storage Account, then removes stale IPs and adds new ones.
Also we are not dealing with random custom IPs. All firewall rules are based on Service Tag IP ranges (downloaded from Microsoft JSON)
I'm using $regionName : "AzureCloud.westeurope
Powershell Code
param(
[Parameter(Mandatory = $true)]
[string]$storageAccountName,
[Parameter(Mandatory = $true)]
[string]$resourceGroupName,
[Parameter(Mandatory = $true)]
[string]$regionName
)
Write-Host "Getting PowerShell Module"
Import-Module Az.Storage -ErrorAction Stop
$ErrorActionPreference = "Stop"
$response = Invoke-WebRequest "https://www.microsoft.com/en-us/download/details.aspx?id=56519"
$fileStartIndex = $response.Content.IndexOf("ServiceTags_Public_")
$fileEndIndex = $response.Content.IndexOf(".json", $fileStartIndex)
$fileName = $response.Content.Substring($fileStartIndex, $fileEndIndex - $fileStartIndex)
$downloadLink = "https://download.microsoft.com/download/7/1/D/71D86715-5596-4529-9B13-DA13A5DE5B63/$fileName.json"
$data = Invoke-RestMethod $downloadLink
$addresses = @()
$filteredAddresses = @()
foreach ($item in $data.values) {
if ($item.name -eq $regionName) {
foreach ($ip in $item.properties.addressPrefixes) {
$addresses += $ip
}
}
}
foreach ($address in $addresses) {
if ($address -notmatch ":") {
$filteredAddresses += $address
}
}
$ipRanges = @()
foreach ($ip in $filteredAddresses) {
if ($ip -match "/31$|/32$") {
$modifiedIp = $ip -replace "/(31|32)$", ""
} else {
$modifiedIp = $ip
}
$ipRanges += $modifiedIp
}
Write-Host "Updating storage account $storageAccountName netowrking"
$beforeIps = (Get-AzStorageAccount -ResourceGroupName $resourceGroupName -Name $storageAccountName).NetworkRuleSet.IpRules | ForEach-Object { $_.IPAddressOrRange }
$staleIps = $beforeIps | Where-Object { $_ -notin $ipRanges }
if ($staleIps) {
Write-Host "Stale IPs found in storage account (not present in downloaded JSON):"
$staleIps | ForEach-Object { Write-Host " - $_" }
foreach ($stale in $staleIps) {
try {
Remove-AzStorageAccountNetworkRule -ResourceGroupName $resourceGroupName -Name $storageAccountName -IPAddressOrRange $stale -ErrorAction Stop | Out-Null
Write-Host "Removed stale IP: $stale"
}
catch {
Write-Host "Error removing $stale - $_"
}
}
} else {
Write-Host "No stale IPs found, storage account is in sync with JSON."
}
$storageAccount = Get-AzStorageAccount -ResourceGroupName $resourceGroupName -Name $storageAccountName
$currentRules = $storageAccount.NetworkRuleSet.IpRules
$currentRuleIps = $currentRules | ForEach-Object { $_.IPAddressOrRange }
$currentCount = $currentRuleIps.Count
$maxLimit = 400
$remaining = $maxLimit - $currentCount
Write-Host "Storage account already has $currentCount rules. Remaining capacity: $remaining"
if ($remaining -le 0) {
Write-Host "No capacity left to add IP rules. Skipping..."
return
}
$newIps = $ipRanges | Where-Object { $currentRuleIps -notcontains $_ }
$toAdd = $newIps | Select-Object -First $remaining
foreach ($range in $toAdd) {
try {
Add-AzStorageAccountNetworkRule -ResourceGroupName $resourceGroupName -Name $storageAccountName -IPAddressOrRange $range -ErrorAction Stop | Out-Null
}
catch {
Write-Host "Error adding $range - $_"
}
}
$afterIps = (Get-AzStorageAccount -ResourceGroupName $resourceGroupName -Name $storageAccountName).NetworkRuleSet.IpRules | ForEach-Object { $_.IPAddressOrRange }
$actuallyAdded = $afterIps | Where-Object { $beforeIps -notcontains $_ }
Write-Host "Newly added IPs: $($actuallyAdded -join ', ')"
if ($ipRanges.Count -gt $remaining) {
$skipped = $ipRanges.Count - $remaining
Write-Host "Skipped $skipped IP(s) because the storage account reached the 400 rule limit."
}
This works functionally, but there is a big problem. Each Remove- or Add- cmdlet call updates the entire Storage Account resource.
That means each individual IP change generates an “Update Storage Account” event in the Azure Activity Log. In my environment, this causes hundreds of alerts to fire, creating a lot of noise.
Is it possible to update the IpRules list in bulk—removing stale entries and adding new ones in a single API call—so that only one ‘Storage Account updated’ event is logged, minimizing alert noise?
You have a couple of options to apply the changes in bulk. You will first need to evaluate any stale or new IPs and combine them before making any changes to the Storage Account. What you're doing at the moment is for running a Remove-AzStorageAccountNetworkRule or a Add-AzStorageAccountNetworkRule for every IP identified.
The most appropriate method to do a single update is to use the Update-AzStorageAccountNetworkRuleSet cmdlet which is specifially for updating the NetworkRule property of a Storage account. As mentioned earlier, you will need to perform the evaluation prior to updating the NetworkRules.
# Prepare changes for bulk update
$changesToApply = @{
ToRemove = @()
ToAdd = @()
}
# Collect stale IPs for removal
if ($staleIps) {
Write-Host "Stale IPs found in storage account (not present in downloaded JSON):"
$staleIps | ForEach-Object {
Write-Host " - $_"
$changesToApply.ToRemove += $_
}
} else {
Write-Host "No stale IPs found, storage account is in sync with JSON."
}
# Collect new IPs for addition (respecting capacity limit)
if ($remaining -gt 0) {
$toAdd = $newIps | Select-Object -First $remaining
$toAdd | ForEach-Object {
$changesToApply.ToAdd += $_
}
if ($newIps.Count -gt $remaining) {
$skipped = $newIps.Count - $remaining
Write-Host "Will skip $skipped IP(s) because the storage account would exceed the 400 rule limit."
}
} else {
Write-Host "No capacity left to add IP rules. Skipping additions..."
}
Followed by the bulk update using Update-AzStorageAccountNetworkRuleSet:
# Apply all changes in a single bulk update operation
if ($changesToApply.ToRemove.Count -gt 0 -or $changesToApply.ToAdd.Count -gt 0) {
Write-Host "Applying bulk update to storage account network rules..."
Write-Host " - Removing $($changesToApply.ToRemove.Count) stale IP(s)"
Write-Host " - Adding $($changesToApply.ToAdd.Count) new IP(s)"
# Get current network rule set
$networkRuleSet = $storageAccount.NetworkRuleSet
# Remove stale IPs from the rule set
if ($changesToApply.ToRemove.Count -gt 0) {
$updatedIpRules = $networkRuleSet.IpRules | Where-Object { $_.IPAddressOrRange -notin $changesToApply.ToRemove }
$networkRuleSet.IpRules = $updatedIpRules
}
# Add new IPs to the rule set
if ($changesToApply.ToAdd.Count -gt 0) {
$newRules = $changesToApply.ToAdd | ForEach-Object {
[Microsoft.Azure.Commands.Management.Storage.Models.PSIpRule]@{
IPAddressOrRange = $_
Action = "Allow"
}
}
$networkRuleSet.IpRules += $newRules
}
# Apply the updated network rule set in one operation
Update-AzStorageAccountNetworkRuleSet -ResourceGroupName $resourceGroupName -Name $storageAccountName -IPRule $networkRuleSet.IpRules -ErrorAction Stop | Out-Null
Write-Host "Bulk update completed successfully!"
# Report what was actually changed
$afterIps = (Get-AzStorageAccount -ResourceGroupName $resourceGroupName -Name $storageAccountName).NetworkRuleSet.IpRules | ForEach-Object { $_.IPAddressOrRange }
$actuallyAdded = $afterIps | Where-Object { $beforeIps -notcontains $_ }
$actuallyRemoved = $beforeIps | Where-Object { $afterIps -notcontains $_ }
if ($actuallyAdded.Count -gt 0) {
Write-Host "Newly added IPs: $($actuallyAdded -join ', ')"
}
if ($actuallyRemoved.Count -gt 0) {
Write-Host "Removed IPs: $($actuallyRemoved -join ', ')"
}
} else {
Write-Host "No changes needed - storage account is already up to date."
}
And this is the result:
You could evaluate and combine per action type, one time for Remove-AzStorageAccountNetworkRule and another for Add-AzStorageAccountNetworkRule. This will result in two activities showing up in activity log. It will effectively remove the stale IPs first from the NetworkRules and then add new ones if any. I'm not entirely sure what your needs are but Option 1 (on bash) satisfies the requirements on my end.