Azure Front Door WAF: update BlockIPs custom rule
Overview
We created a PowerShell script to safely add a list of IP addresses to an existing Azure Front Door Web Application Firewall policy custom rule.
The script is designed to:
- Connect to the correct Azure subscription.
- Retrieve the existing Azure Front Door WAF policy.
- Find the custom rule named
BlockIPs. - Find the rule condition using
RemoteAddrwith theIPMatchoperator. - Preserve all existing blocked IP addresses.
- Append the new IP addresses.
- Remove duplicates.
- Validate the supplied IP addresses.
- Export a backup of the current WAF policy.
- Export the proposed updated request body.
- Display the exact addresses that will be added.
- Require confirmation before applying the update.
- Retrieve the policy again after the change and verify that all requested IPs are present.
Azure resources
| Setting | Value |
|---|---|
| Subscription ID | 00000000-0000-0000-0000-000000000000 |
| Resource group | rg-example-afd |
| WAF policy | waf-example-afd |
| Custom rule | BlockIPs |
| WAF type | Azure Front Door WAF |
IP addresses added
203.0.113.10
203.0.113.11
198.51.100.25
198.51.100.26
192.0.2.40
192.0.2.41Why the REST API was used
The Azure Front Door WAF policy is exposed through the Azure resource provider:
Microsoft.Network/FrontDoorWebApplicationFirewallPoliciesThe script uses Invoke-AzRestMethod to retrieve and update the existing policy object directly.
This approach avoids rebuilding the complete custom rule manually with PowerShell object constructors and reduces the risk of unintentionally losing existing rule configuration.
Prerequisites
The script requires the Az.Accounts PowerShell module.
Install-Module Az.Accounts -Scope CurrentUserYou must also have permissions to read and update the Azure Front Door WAF policy.
Suitable permissions include:
- Contributor on the WAF policy or resource group.
- Network Contributor where appropriate.
- A custom role containing read and write access for
Microsoft.Network/FrontDoorWebApplicationFirewallPolicies.
How to run
Save the script as:
Add-WafBlockedIPs.ps1Run it from Azure Cloud Shell PowerShell or from a machine with the Azure PowerShell modules installed:
.\Add-WafBlockedIPs.ps1The script displays the current number of IP entries, the addresses already present, and the new addresses that will be added.
Before changing Azure, it requires the operator to type:
APPLYFor non-interactive execution, use:
.\Add-WafBlockedIPs.ps1 -ForceGenerated files
The script creates two JSON files in the current working directory.
Current policy backup
waf-example-afd-backup-YYYYMMDD-HHMMSS.jsonThis contains the WAF policy as it existed before the change.
Proposed request body
waf-example-afd-proposed-YYYYMMDD-HHMMSS.jsonThis contains the JSON body that will be sent to Azure.
Safety controls
The script stops without changing Azure when:
- The WAF policy cannot be retrieved.
- The custom rule
BlockIPscannot be found. - More than one rule named
BlockIPsis returned. - No
RemoteAddrandIPMatchcondition exists. - More than one
RemoteAddrandIPMatchcondition exists. - Any supplied IP address is invalid.
- The operator does not type
APPLY. - Azure rejects the update.
The script also warns when the custom rule action is not currently set to Block.
PowerShell script
#Requires -Modules Az.Accounts
<#
.SYNOPSIS
Adds IP addresses to an existing Azure Front Door WAF custom rule.
.DESCRIPTION
- Retrieves the existing WAF policy
- Finds the specified custom rule
- Finds its RemoteAddr/IPMatch condition
- Preserves existing IP addresses
- Adds the new IP addresses
- Removes duplicates
- Saves a local JSON backup
- Shows the proposed changes
- Requires confirmation before updating Azure
#>
[CmdletBinding()]
param(
[switch]$Force
)
$ErrorActionPreference = 'Stop'
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
$SubscriptionId = '00000000-0000-0000-0000-000000000000'
$ResourceGroupName = 'rg-example-afd'
$WafPolicyName = 'waf-example-afd'
$CustomRuleName = 'BlockIPs'
$ApiVersion = '2022-05-01'
$NewIpAddresses = @(
'203.0.113.10'
'203.0.113.11'
'198.51.100.25'
'198.51.100.26'
'192.0.2.40'
'192.0.2.41'
)
# ---------------------------------------------------------------------------
# Login and subscription context
# ---------------------------------------------------------------------------
$context = Get-AzContext
if (-not $context) {
Write-Host 'No Azure session found. Launching interactive login...' -ForegroundColor Yellow
Connect-AzAccount | Out-Null
}
Set-AzContext -SubscriptionId $SubscriptionId | Out-Null
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
function Test-IpAddress {
param(
[Parameter(Mandatory)]
[string]$Ip
)
$parsed = $null
return [System.Net.IPAddress]::TryParse($Ip, [ref]$parsed)
}
function Save-JsonFile {
param(
[Parameter(Mandatory)]
[string]$Path,
[Parameter(Mandatory)]
$Object
)
$Object | ConvertTo-Json -Depth 100 | Set-Content -Path $Path -Encoding utf8
}
# ---------------------------------------------------------------------------
# Validate input IPs
# ---------------------------------------------------------------------------
$InvalidIps = $NewIpAddresses | Where-Object { -not (Test-IpAddress $_) }
if ($InvalidIps) {
throw "Invalid IP address(es) supplied: $($InvalidIps -join ', ')"
}
# ---------------------------------------------------------------------------
# Build resource ID and URI
# ---------------------------------------------------------------------------
$ResourceId = "/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroupName/providers/Microsoft.Network/FrontDoorWebApplicationFirewallPolicies/$WafPolicyName"
$Uri = "$ResourceId?api-version=$ApiVersion"
Write-Host "Retrieving WAF policy: $WafPolicyName" -ForegroundColor Cyan
$GetResponse = Invoke-AzRestMethod -Method GET -Path $Uri
if (-not $GetResponse -or -not $GetResponse.Content) {
throw 'Failed to retrieve the WAF policy or received an empty response.'
}
$Policy = $GetResponse.Content | ConvertFrom-Json -Depth 100
# ---------------------------------------------------------------------------
# Backup current policy
# ---------------------------------------------------------------------------
$Timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$BackupPath = ".\$($WafPolicyName)-backup-$Timestamp.json"
Save-JsonFile -Path $BackupPath -Object $Policy
Write-Host "Backup saved to: $BackupPath" -ForegroundColor Green
# ---------------------------------------------------------------------------
# Locate custom rule
# ---------------------------------------------------------------------------
$CustomRules = @($Policy.properties.customRules.rules | Where-Object { $_.name -eq $CustomRuleName })
if ($CustomRules.Count -eq 0) {
throw "Custom rule '$CustomRuleName' was not found."
}
if ($CustomRules.Count -gt 1) {
throw "More than one custom rule named '$CustomRuleName' was found. Aborting."
}
$Rule = $CustomRules[0]
if ($Rule.action -ne 'Block') {
Write-Warning "Custom rule '$CustomRuleName' action is '$($Rule.action)' instead of 'Block'."
}
# ---------------------------------------------------------------------------
# Locate RemoteAddr/IPMatch condition
# ---------------------------------------------------------------------------
$MatchingConditions = @(
$Rule.matchConditions | Where-Object {
$_.matchVariable -eq 'RemoteAddr' -and $_.operator -eq 'IPMatch'
}
)
if ($MatchingConditions.Count -eq 0) {
throw "No RemoteAddr/IPMatch condition found in custom rule '$CustomRuleName'."
}
if ($MatchingConditions.Count -gt 1) {
throw "More than one RemoteAddr/IPMatch condition found in custom rule '$CustomRuleName'. Aborting."
}
$Condition = $MatchingConditions[0]
# ---------------------------------------------------------------------------
# Merge IPs
# ---------------------------------------------------------------------------
$ExistingIps = @($Condition.matchValue)
$MergedIps = @($ExistingIps + $NewIpAddresses | Sort-Object -Unique)
$IpsToAdd = @($MergedIps | Where-Object { $_ -notin $ExistingIps })
Write-Host "Existing IP count : $($ExistingIps.Count)" -ForegroundColor Cyan
Write-Host "Requested IP count: $($NewIpAddresses.Count)" -ForegroundColor Cyan
Write-Host "New IPs to add : $($IpsToAdd.Count)" -ForegroundColor Cyan
if ($IpsToAdd.Count -eq 0) {
Write-Host 'All requested IPs already exist in the rule. No change required.' -ForegroundColor Yellow
return
}
Write-Host ''
Write-Host 'IPs that will be added:' -ForegroundColor Yellow
$IpsToAdd | ForEach-Object { Write-Host " - $_" }
$Condition.matchValue = $MergedIps
# ---------------------------------------------------------------------------
# Save proposed body
# ---------------------------------------------------------------------------
$ProposedPath = ".\$($WafPolicyName)-proposed-$Timestamp.json"
Save-JsonFile -Path $ProposedPath -Object $Policy
Write-Host "Proposed request body saved to: $ProposedPath" -ForegroundColor Green
# ---------------------------------------------------------------------------
# Confirm
# ---------------------------------------------------------------------------
if (-not $Force) {
Write-Host ''
$confirmation = Read-Host "Type APPLY to update Azure Front Door WAF policy '$WafPolicyName'"
if ($confirmation -ne 'APPLY') {
Write-Host 'Confirmation not received. No changes applied.' -ForegroundColor Yellow
return
}
}
# ---------------------------------------------------------------------------
# Update policy
# ---------------------------------------------------------------------------
Write-Host 'Applying update to Azure...' -ForegroundColor Cyan
$PutBody = $Policy | ConvertTo-Json -Depth 100
$UpdateResponse = Invoke-AzRestMethod -Method PUT -Path $Uri -Payload $PutBody
if (-not $UpdateResponse) {
throw 'Azure returned an empty response to the update request.'
}
Write-Host 'Update submitted successfully.' -ForegroundColor Green
# ---------------------------------------------------------------------------
# Verify
# ---------------------------------------------------------------------------
Write-Host 'Verifying updated policy...' -ForegroundColor Cyan
$VerifyResponse = Invoke-AzRestMethod -Method GET -Path $Uri
$VerifiedPolicy = $VerifyResponse.Content | ConvertFrom-Json -Depth 100
$VerifiedRule = @($VerifiedPolicy.properties.customRules.rules | Where-Object { $_.name -eq $CustomRuleName })
if ($VerifiedRule.Count -ne 1) {
throw 'Unable to uniquely locate the custom rule after update.'
}
$VerifiedCondition = @(
$VerifiedRule[0].matchConditions | Where-Object {
$_.matchVariable -eq 'RemoteAddr' -and $_.operator -eq 'IPMatch'
}
)
if ($VerifiedCondition.Count -ne 1) {
throw 'Unable to uniquely locate the RemoteAddr/IPMatch condition after update.'
}
$VerifiedIps = @($VerifiedCondition[0].matchValue)
$MissingAfterUpdate = @($NewIpAddresses | Where-Object { $_ -notin $VerifiedIps })
if ($MissingAfterUpdate.Count -gt 0) {
throw "Update completed, but these IPs were not found in the verification step: $($MissingAfterUpdate -join ', ')"
}
Write-Host 'Verification successful. All requested IPs are present.' -ForegroundColor Green