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 RemoteAddr with the IPMatch operator.
  • 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

SettingValue
Subscription ID00000000-0000-0000-0000-000000000000
Resource grouprg-example-afd
WAF policywaf-example-afd
Custom ruleBlockIPs
WAF typeAzure 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.41

Why the REST API was used

The Azure Front Door WAF policy is exposed through the Azure resource provider:

Microsoft.Network/FrontDoorWebApplicationFirewallPolicies

The 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 CurrentUser

You 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.ps1

Run it from Azure Cloud Shell PowerShell or from a machine with the Azure PowerShell modules installed:

.\Add-WafBlockedIPs.ps1

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

APPLY

For non-interactive execution, use:

.\Add-WafBlockedIPs.ps1 -Force

Generated files

The script creates two JSON files in the current working directory.

Current policy backup

waf-example-afd-backup-YYYYMMDD-HHMMSS.json

This contains the WAF policy as it existed before the change.

Proposed request body

waf-example-afd-proposed-YYYYMMDD-HHMMSS.json

This 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 BlockIPs cannot be found.
  • More than one rule named BlockIPs is returned.
  • No RemoteAddr and IPMatch condition exists.
  • More than one RemoteAddr and IPMatch condition 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