Azure Web Application Firewall (WAF) protects web applications from common exploits and vulnerabilities including SQL injection, cross-site scripting, and other OWASP Top 10 threats. This guide covers deploying WAF on both Application Gateway and Azure Front Door with managed rules, custom rules, and bot protection.
This article is part of our comprehensive cloud security tips guide, focusing specifically on Azure web application protection.
Overview
Azure WAF can be deployed on three services:
- Application Gateway: Regional layer 7 load balancer with WAF
- Azure Front Door: Global CDN and load balancer with edge WAF
- Azure CDN: Content delivery network with WAF capabilities
This guide focuses on Application Gateway and Front Door, the most common deployment options.
Prerequisites
Before deploying Azure WAF:
- Azure subscription with Contributor access
- Virtual Network with dedicated subnet for Application Gateway
- Backend application to protect (App Service, VMs, AKS, etc.)
- Azure CLI (2.50.0+) or Azure Portal access
- Public DNS for custom domain configuration (optional)
- Understanding of HTTP/HTTPS traffic patterns for your application
Part 1: WAF on Application Gateway
Create Application Gateway with WAF
Step 1: Prepare Networking
# Create resource group
az group create \
--name rg-waf \
--location eastus
# Create VNet
az network vnet create \
--name vnet-waf \
--resource-group rg-waf \
--location eastus \
--address-prefix 10.0.0.0/16
# Create subnet for Application Gateway (minimum /24 recommended)
az network vnet subnet create \
--name snet-appgw \
--resource-group rg-waf \
--vnet-name vnet-waf \
--address-prefix 10.0.1.0/24
# Create public IP
az network public-ip create \
--name pip-appgw-waf \
--resource-group rg-waf \
--location eastus \
--allocation-method Static \
--sku Standard \
--zone 1 2 3
Step 2: Create WAF Policy
# Create WAF policy with OWASP 3.2 rules
az network application-gateway waf-policy create \
--name waf-policy-appgw \
--resource-group rg-waf \
--location eastus
# Configure managed rules
az network application-gateway waf-policy managed-rule rule-set add \
--policy-name waf-policy-appgw \
--resource-group rg-waf \
--type OWASP \
--version 3.2
# Add Microsoft Bot Manager rule set
az network application-gateway waf-policy managed-rule rule-set add \
--policy-name waf-policy-appgw \
--resource-group rg-waf \
--type Microsoft_BotManagerRuleSet \
--version 1.0
# Set policy to detection mode initially
az network application-gateway waf-policy policy-setting update \
--policy-name waf-policy-appgw \
--resource-group rg-waf \
--mode Detection \
--state Enabled \
--request-body-check true \
--max-request-body-size-in-kb 128 \
--file-upload-limit-in-mb 100
Step 3: Create Application Gateway with WAF v2
# Create Application Gateway WAF v2
az network application-gateway create \
--name appgw-waf \
--resource-group rg-waf \
--location eastus \
--sku WAF_v2 \
--capacity 2 \
--vnet-name vnet-waf \
--subnet snet-appgw \
--public-ip-address pip-appgw-waf \
--http-settings-cookie-based-affinity Disabled \
--http-settings-port 80 \
--http-settings-protocol Http \
--frontend-port 80 \
--routing-rule-type Basic \
--servers 10.0.2.4 10.0.2.5 \
--waf-policy waf-policy-appgw \
--zones 1 2 3
# Add HTTPS listener (requires certificate)
# First, create Key Vault and upload certificate
az keyvault create \
--name kv-appgw-certs \
--resource-group rg-waf \
--location eastus \
--enable-soft-delete true \
--enable-purge-protection true
# Enable managed identity for App Gateway
az network application-gateway identity assign \
--gateway-name appgw-waf \
--resource-group rg-waf \
--identity $(az identity create \
--name mi-appgw \
--resource-group rg-waf \
--query id -o tsv)
# Grant Key Vault access to App Gateway identity
APPGW_IDENTITY=$(az network application-gateway show \
--name appgw-waf \
--resource-group rg-waf \
--query "identity.userAssignedIdentities.*.principalId" -o tsv)
az keyvault set-policy \
--name kv-appgw-certs \
--object-id $APPGW_IDENTITY \
--secret-permissions get
# Add SSL certificate from Key Vault
az network application-gateway ssl-cert create \
--gateway-name appgw-waf \
--resource-group rg-waf \
--name ssl-cert-main \
--key-vault-secret-id $(az keyvault secret show \
--vault-name kv-appgw-certs \
--name my-ssl-cert \
--query id -o tsv)
# Add HTTPS frontend port
az network application-gateway frontend-port create \
--gateway-name appgw-waf \
--resource-group rg-waf \
--name https-port \
--port 443
# Add HTTPS listener
az network application-gateway http-listener create \
--gateway-name appgw-waf \
--resource-group rg-waf \
--name https-listener \
--frontend-port https-port \
--frontend-ip appGatewayFrontendIP \
--ssl-cert ssl-cert-main
Configure Custom WAF Rules
# Create custom rule to block specific countries
az network application-gateway waf-policy custom-rule create \
--policy-name waf-policy-appgw \
--resource-group rg-waf \
--name BlockHighRiskCountries \
--priority 10 \
--rule-type MatchRule \
--action Block
az network application-gateway waf-policy custom-rule match-condition add \
--policy-name waf-policy-appgw \
--resource-group rg-waf \
--name BlockHighRiskCountries \
--match-variables RemoteAddr \
--operator GeoMatch \
--values CN RU KP IR
# Create rate limiting rule
az network application-gateway waf-policy custom-rule create \
--policy-name waf-policy-appgw \
--resource-group rg-waf \
--name RateLimitRule \
--priority 20 \
--rule-type RateLimitRule \
--action Block \
--rate-limit-threshold 100 \
--rate-limit-duration OneMin \
--group-by-user-session "[{\"groupByVariables\":[{\"variableName\":\"ClientAddr\"}]}]"
# Block requests with suspicious headers
az network application-gateway waf-policy custom-rule create \
--policy-name waf-policy-appgw \
--resource-group rg-waf \
--name BlockSuspiciousHeaders \
--priority 30 \
--rule-type MatchRule \
--action Block
az network application-gateway waf-policy custom-rule match-condition add \
--policy-name waf-policy-appgw \
--resource-group rg-waf \
--name BlockSuspiciousHeaders \
--match-variables "RequestHeaders['User-Agent']" \
--operator Contains \
--values "sqlmap" "nikto" "nmap" "masscan" \
--transforms Lowercase
Configure Exclusions
Handle false positives by excluding specific request attributes:
# Exclude specific request header from all rules
az network application-gateway waf-policy managed-rule exclusion add \
--policy-name waf-policy-appgw \
--resource-group rg-waf \
--match-variable RequestHeaderNames \
--selector-match-operator Equals \
--selector "X-Custom-Token"
# Exclude request body field from specific rule
az network application-gateway waf-policy managed-rule exclusion add \
--policy-name waf-policy-appgw \
--resource-group rg-waf \
--match-variable RequestBodyPostArgNames \
--selector-match-operator Contains \
--selector "message" \
--exclusion-rule-set-type OWASP \
--exclusion-rule-set-version 3.2 \
--exclusion-rule-group REQUEST-942-APPLICATION-ATTACK-SQLI \
--exclusion-rule-ids 942130 942430
Enable Prevention Mode
After tuning in Detection mode:
# Switch to Prevention mode
az network application-gateway waf-policy policy-setting update \
--policy-name waf-policy-appgw \
--resource-group rg-waf \
--mode Prevention
Part 2: WAF on Azure Front Door
Create Front Door with WAF
Step 1: Create WAF Policy for Front Door
# Create Front Door WAF policy
az network front-door waf-policy create \
--name waf-policy-fd \
--resource-group rg-waf \
--mode Detection \
--redirect-url "https://www.yoursite.com/blocked" \
--custom-block-response-status-code 403 \
--custom-block-response-body "VGhpcyByZXF1ZXN0IGhhcyBiZWVuIGJsb2NrZWQgYnkgV0FG"
# Add managed rules
az network front-door waf-policy managed-rule-definition list
# Lists available rule sets
az network front-door waf-policy managed-rules add \
--policy-name waf-policy-fd \
--resource-group rg-waf \
--type DefaultRuleSet \
--version "2.1"
az network front-door waf-policy managed-rules add \
--policy-name waf-policy-fd \
--resource-group rg-waf \
--type Microsoft_BotManagerRuleSet \
--version "1.0"
Step 2: Create Front Door Profile
# Create Front Door Standard/Premium
az afd profile create \
--profile-name fd-waf \
--resource-group rg-waf \
--sku Premium_AzureFrontDoor
# Create endpoint
az afd endpoint create \
--endpoint-name fd-endpoint \
--profile-name fd-waf \
--resource-group rg-waf \
--enabled-state Enabled
# Create origin group
az afd origin-group create \
--origin-group-name og-webapp \
--profile-name fd-waf \
--resource-group rg-waf \
--probe-request-type GET \
--probe-protocol Https \
--probe-path "/health" \
--probe-interval-in-seconds 30 \
--sample-size 4 \
--successful-samples-required 3 \
--additional-latency-in-milliseconds 50
# Add origin
az afd origin create \
--origin-name origin-webapp \
--origin-group-name og-webapp \
--profile-name fd-waf \
--resource-group rg-waf \
--host-name mywebapp.azurewebsites.net \
--origin-host-header mywebapp.azurewebsites.net \
--http-port 80 \
--https-port 443 \
--priority 1 \
--weight 1000 \
--enabled-state Enabled
# Create route
az afd route create \
--route-name route-default \
--endpoint-name fd-endpoint \
--profile-name fd-waf \
--resource-group rg-waf \
--origin-group og-webapp \
--supported-protocols Https Http \
--patterns-to-match "/*" \
--forwarding-protocol HttpsOnly \
--https-redirect Enabled
Step 3: Create Security Policy
# Get WAF policy ID
WAF_POLICY_ID=$(az network front-door waf-policy show \
--name waf-policy-fd \
--resource-group rg-waf \
--query id -o tsv)
# Create security policy (links WAF to Front Door)
az afd security-policy create \
--security-policy-name sec-policy-waf \
--profile-name fd-waf \
--resource-group rg-waf \
--waf-policy $WAF_POLICY_ID \
--domains "/subscriptions/{sub-id}/resourcegroups/rg-waf/providers/Microsoft.Cdn/profiles/fd-waf/afdEndpoints/fd-endpoint"
Configure Front Door Custom Rules
# Create custom rule for geo-blocking
az network front-door waf-policy rule create \
--policy-name waf-policy-fd \
--resource-group rg-waf \
--name GeoBlockRule \
--priority 100 \
--action Block \
--rule-type MatchRule \
--match-conditions '[{
"matchVariable": "RemoteAddr",
"operator": "GeoMatch",
"matchValue": ["CN", "RU", "KP"]
}]'
# Create rate limiting rule
az network front-door waf-policy rule create \
--policy-name waf-policy-fd \
--resource-group rg-waf \
--name RateLimitByIP \
--priority 200 \
--action Block \
--rule-type RateLimitRule \
--rate-limit-duration-in-minutes 1 \
--rate-limit-threshold 100 \
--match-conditions '[{
"matchVariable": "RequestUri",
"operator": "RegEx",
"matchValue": ["/api/.*"]
}]'
# Allow trusted IPs (whitelist)
az network front-door waf-policy rule create \
--policy-name waf-policy-fd \
--resource-group rg-waf \
--name AllowTrustedIPs \
--priority 50 \
--action Allow \
--rule-type MatchRule \
--match-conditions '[{
"matchVariable": "RemoteAddr",
"operator": "IPMatch",
"matchValue": ["203.0.113.0/24", "198.51.100.0/24"]
}]'
# Block specific user agents
az network front-door waf-policy rule create \
--policy-name waf-policy-fd \
--resource-group rg-waf \
--name BlockBadBots \
--priority 150 \
--action Block \
--rule-type MatchRule \
--match-conditions '[{
"matchVariable": "RequestHeader",
"selector": "User-Agent",
"operator": "Contains",
"transforms": ["Lowercase"],
"matchValue": ["sqlmap", "nikto", "masscan", "nmap"]
}]'
Configure Bot Protection
# Enable bot manager with specific actions
az network front-door waf-policy managed-rules add \
--policy-name waf-policy-fd \
--resource-group rg-waf \
--type Microsoft_BotManagerRuleSet \
--version "1.0"
# Override specific bot rule actions
az network front-door waf-policy managed-rules override add \
--policy-name waf-policy-fd \
--resource-group rg-waf \
--type Microsoft_BotManagerRuleSet \
--rule-group-id GoodBots \
--action Log
az network front-door waf-policy managed-rules override add \
--policy-name waf-policy-fd \
--resource-group rg-waf \
--type Microsoft_BotManagerRuleSet \
--rule-group-id BadBots \
--action Block
Part 3: Monitoring and Logging
Enable Diagnostic Logs
# For Application Gateway WAF
az monitor diagnostic-settings create \
--name appgw-waf-logs \
--resource $(az network application-gateway show \
--name appgw-waf \
--resource-group rg-waf \
--query id -o tsv) \
--logs '[
{"category": "ApplicationGatewayAccessLog", "enabled": true},
{"category": "ApplicationGatewayPerformanceLog", "enabled": true},
{"category": "ApplicationGatewayFirewallLog", "enabled": true}
]' \
--workspace $(az monitor log-analytics workspace show \
--name law-security \
--resource-group rg-waf \
--query id -o tsv)
# For Front Door WAF
az monitor diagnostic-settings create \
--name fd-waf-logs \
--resource $(az afd profile show \
--profile-name fd-waf \
--resource-group rg-waf \
--query id -o tsv) \
--logs '[
{"category": "FrontDoorAccessLog", "enabled": true},
{"category": "FrontDoorHealthProbeLog", "enabled": true},
{"category": "FrontDoorWebApplicationFirewallLog", "enabled": true}
]' \
--workspace $(az monitor log-analytics workspace show \
--name law-security \
--resource-group rg-waf \
--query id -o tsv)
Log Analytics Queries
// WAF blocked requests by rule
AzureDiagnostics
| where ResourceProvider == "MICROSOFT.NETWORK"
| where Category == "ApplicationGatewayFirewallLog"
| where action_s == "Blocked"
| summarize count() by ruleId_s, ruleGroup_s
| order by count_ desc
// Top blocked IPs
AzureDiagnostics
| where Category == "ApplicationGatewayFirewallLog"
| where action_s == "Blocked"
| summarize BlockCount = count() by clientIp_s
| order by BlockCount desc
| take 20
// WAF attack patterns over time
AzureDiagnostics
| where Category == "ApplicationGatewayFirewallLog"
| where action_s == "Blocked"
| summarize count() by bin(TimeGenerated, 1h), ruleGroup_s
| render timechart
// Potential SQL injection attempts
AzureDiagnostics
| where Category == "ApplicationGatewayFirewallLog"
| where ruleGroup_s == "REQUEST-942-APPLICATION-ATTACK-SQLI"
| project TimeGenerated, clientIp_s, requestUri_s, Message
| take 100
// Bot traffic analysis
AzureDiagnostics
| where Category == "FrontDoorWebApplicationFirewallLog"
| where ruleName_s contains "Bot"
| summarize count() by ruleName_s, action_s
| order by count_ desc
Create Alerts
# Alert on high block rate
az monitor scheduled-query create \
--name "WAF High Block Rate" \
--resource-group rg-waf \
--scopes $(az network application-gateway show \
--name appgw-waf \
--resource-group rg-waf \
--query id -o tsv) \
--condition "count > 100" \
--condition-query "AzureDiagnostics
| where Category == 'ApplicationGatewayFirewallLog'
| where action_s == 'Blocked'
| where TimeGenerated > ago(5m)" \
--description "High WAF block rate detected" \
--evaluation-frequency "5m" \
--window-size "5m" \
--severity 2 \
--action-groups "/subscriptions/{sub-id}/resourceGroups/rg-waf/providers/microsoft.insights/actionGroups/SecurityAlerts"
# Alert on potential attack from single IP
az monitor scheduled-query create \
--name "WAF Single IP Attack" \
--resource-group rg-waf \
--scopes $(az network application-gateway show \
--name appgw-waf \
--resource-group rg-waf \
--query id -o tsv) \
--condition "count > 50" \
--condition-query "AzureDiagnostics
| where Category == 'ApplicationGatewayFirewallLog'
| where action_s == 'Blocked'
| where TimeGenerated > ago(5m)
| summarize count() by clientIp_s
| where count_ > 50" \
--description "Potential attack from single IP" \
--evaluation-frequency "5m" \
--window-size "5m" \
--severity 1
Part 4: Terraform Configuration
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.85"
}
}
}
provider "azurerm" {
features {}
}
# Variables
variable "location" {
default = "eastus"
}
# Resource Group
resource "azurerm_resource_group" "waf" {
name = "rg-waf"
location = var.location
}
# WAF Policy for Application Gateway
resource "azurerm_web_application_firewall_policy" "appgw" {
name = "waf-policy-appgw"
resource_group_name = azurerm_resource_group.waf.name
location = var.location
policy_settings {
enabled = true
mode = "Detection" # Change to "Prevention" after tuning
request_body_check = true
file_upload_limit_in_mb = 100
max_request_body_size_in_kb = 128
}
managed_rules {
managed_rule_set {
type = "OWASP"
version = "3.2"
}
managed_rule_set {
type = "Microsoft_BotManagerRuleSet"
version = "1.0"
}
exclusion {
match_variable = "RequestHeaderNames"
selector = "X-Auth-Token"
selector_match_operator = "Equals"
}
}
custom_rules {
name = "BlockHighRiskCountries"
priority = 10
rule_type = "MatchRule"
action = "Block"
match_conditions {
match_variables {
variable_name = "RemoteAddr"
}
operator = "GeoMatch"
negation_condition = false
match_values = ["CN", "RU", "KP", "IR"]
}
}
custom_rules {
name = "RateLimitRule"
priority = 20
rule_type = "RateLimitRule"
action = "Block"
rate_limit_duration_in_minutes = 1
rate_limit_threshold = 100
match_conditions {
match_variables {
variable_name = "RemoteAddr"
}
operator = "IPMatch"
negation_condition = true
match_values = ["127.0.0.1"] # Exclude localhost
}
}
custom_rules {
name = "AllowTrustedIPs"
priority = 5
rule_type = "MatchRule"
action = "Allow"
match_conditions {
match_variables {
variable_name = "RemoteAddr"
}
operator = "IPMatch"
negation_condition = false
match_values = ["203.0.113.0/24", "198.51.100.0/24"]
}
}
}
# Virtual Network
resource "azurerm_virtual_network" "waf" {
name = "vnet-waf"
resource_group_name = azurerm_resource_group.waf.name
location = var.location
address_space = ["10.0.0.0/16"]
}
resource "azurerm_subnet" "appgw" {
name = "snet-appgw"
resource_group_name = azurerm_resource_group.waf.name
virtual_network_name = azurerm_virtual_network.waf.name
address_prefixes = ["10.0.1.0/24"]
}
# Public IP
resource "azurerm_public_ip" "appgw" {
name = "pip-appgw-waf"
resource_group_name = azurerm_resource_group.waf.name
location = var.location
allocation_method = "Static"
sku = "Standard"
zones = ["1", "2", "3"]
}
# Application Gateway with WAF
resource "azurerm_application_gateway" "waf" {
name = "appgw-waf"
resource_group_name = azurerm_resource_group.waf.name
location = var.location
zones = ["1", "2", "3"]
sku {
name = "WAF_v2"
tier = "WAF_v2"
}
autoscale_configuration {
min_capacity = 2
max_capacity = 10
}
gateway_ip_configuration {
name = "gateway-ip-config"
subnet_id = azurerm_subnet.appgw.id
}
frontend_ip_configuration {
name = "frontend-ip-config"
public_ip_address_id = azurerm_public_ip.appgw.id
}
frontend_port {
name = "http-port"
port = 80
}
frontend_port {
name = "https-port"
port = 443
}
backend_address_pool {
name = "backend-pool"
}
backend_http_settings {
name = "backend-http-settings"
cookie_based_affinity = "Disabled"
port = 80
protocol = "Http"
request_timeout = 30
}
http_listener {
name = "http-listener"
frontend_ip_configuration_name = "frontend-ip-config"
frontend_port_name = "http-port"
protocol = "Http"
}
request_routing_rule {
name = "routing-rule"
priority = 100
rule_type = "Basic"
http_listener_name = "http-listener"
backend_address_pool_name = "backend-pool"
backend_http_settings_name = "backend-http-settings"
}
firewall_policy_id = azurerm_web_application_firewall_policy.appgw.id
depends_on = [azurerm_public_ip.appgw]
}
# Front Door WAF Policy
resource "azurerm_cdn_frontdoor_firewall_policy" "fd" {
name = "wafpolicyfd"
resource_group_name = azurerm_resource_group.waf.name
sku_name = "Premium_AzureFrontDoor"
enabled = true
mode = "Detection"
managed_rule {
type = "DefaultRuleSet"
version = "2.1"
action = "Block"
}
managed_rule {
type = "Microsoft_BotManagerRuleSet"
version = "1.0"
action = "Block"
}
custom_rule {
name = "RateLimitByIP"
enabled = true
priority = 100
type = "RateLimitRule"
action = "Block"
rate_limit_duration_in_minutes = 1
rate_limit_threshold = 100
match_condition {
match_variable = "RequestUri"
operator = "RegEx"
match_values = ["/api/.*"]
}
}
}
# Front Door Profile
resource "azurerm_cdn_frontdoor_profile" "main" {
name = "fd-waf"
resource_group_name = azurerm_resource_group.waf.name
sku_name = "Premium_AzureFrontDoor"
}
# Front Door Endpoint
resource "azurerm_cdn_frontdoor_endpoint" "main" {
name = "fd-endpoint"
cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.main.id
}
# Security Policy (links WAF to Front Door)
resource "azurerm_cdn_frontdoor_security_policy" "waf" {
name = "security-policy-waf"
cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.main.id
security_policies {
firewall {
cdn_frontdoor_firewall_policy_id = azurerm_cdn_frontdoor_firewall_policy.fd.id
association {
domain {
cdn_frontdoor_domain_id = azurerm_cdn_frontdoor_endpoint.main.id
}
patterns_to_match = ["/*"]
}
}
}
}
# Outputs
output "appgw_public_ip" {
value = azurerm_public_ip.appgw.ip_address
}
output "frontdoor_endpoint" {
value = azurerm_cdn_frontdoor_endpoint.main.host_name
}
Best Practices Summary
- Start in Detection mode - Tune for 2-4 weeks before Prevention
- Use the latest rule sets - OWASP 3.2 or DRS 2.1 for best protection
- Enable bot protection - Microsoft Bot Manager catches malicious bots
- Implement rate limiting - Protect APIs and login endpoints
- Create exclusions carefully - Document each exclusion with justification
- Monitor actively - Set up alerts for anomalous traffic patterns
- Use geo-blocking judiciously - Consider business requirements
- Combine with other controls - WAF is one layer of defense
- Regular rule review - Update exclusions and custom rules periodically
- Test your WAF - Use security scanners to validate protection
Troubleshooting
Issue: Legitimate Traffic Being Blocked
# Check WAF logs for specific blocked requests
az monitor log-analytics query \
--workspace law-security \
--analytics-query "AzureDiagnostics
| where Category == 'ApplicationGatewayFirewallLog'
| where action_s == 'Blocked'
| where TimeGenerated > ago(1h)
| project TimeGenerated, clientIp_s, requestUri_s, ruleId_s, Message
| take 100"
Issue: WAF Not Blocking Known Threats
# Verify WAF policy is attached
az network application-gateway show \
--name appgw-waf \
--resource-group rg-waf \
--query "firewallPolicy.id"
# Verify policy is in Prevention mode
az network application-gateway waf-policy show \
--name waf-policy-appgw \
--resource-group rg-waf \
--query "policySettings.mode"