Azure WAF's managed rule sets (DRS, OWASP CRS) handle common attacks, but custom rules let you respond to specific threats you're seeing in logs.
Why Custom Rules?
Managed rules are generic. Custom rules let you:
- Block specific attacker IPs
- Rate limit aggressive scanners
- Block requests to paths that don't exist
- Deny known malicious user agents
- Geo-block traffic from certain countries
Blocking Scanner Paths
Attackers probe for common vulnerabilities. Block requests to paths that don't exist:
resource "azurerm_web_application_firewall_policy" "this" {
name = "waf-policy"
resource_group_name = azurerm_resource_group.this.name
location = azurerm_resource_group.this.location
custom_rules {
name = "BlockScannerPaths"
priority = 10
rule_type = "MatchRule"
action = "Block"
match_conditions {
match_variables {
variable_name = "RequestUri"
}
operator = "Contains"
negation_condition = false
match_values = [
"/wp-admin",
"/wp-login.php",
"/phpmyadmin",
"/.env",
"/.git",
"/xmlrpc.php",
"/admin/config",
"/backup",
"/shell"
]
}
}
}
Rate Limiting
Block IPs making too many requests:
custom_rules {
name = "RateLimitByIP"
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 = ["10.0.0.0/8", "192.168.0.0/16"] # Don't limit internal
}
}
This blocks any external IP making more than 100 requests per minute.
Blocking Bad User Agents
Known scanners often identify themselves:
custom_rules {
name = "BlockBadUserAgents"
priority = 30
rule_type = "MatchRule"
action = "Block"
match_conditions {
match_variables {
variable_name = "RequestHeaders"
selector = "User-Agent"
}
operator = "Contains"
negation_condition = false
match_values = [
"sqlmap",
"nikto",
"nessus",
"nmap",
"masscan",
"dirbuster",
"gobuster",
"wpscan"
]
transforms = ["Lowercase"]
}
}
Geo-Blocking
Block traffic from countries you don't serve:
custom_rules {
name = "GeoBlock"
priority = 5
rule_type = "MatchRule"
action = "Block"
match_conditions {
match_variables {
variable_name = "RemoteAddr"
}
operator = "GeoMatch"
negation_condition = false
match_values = ["RU", "CN", "KP", "IR"] # Country codes
}
}
Allow List for APIs
For APIs that should only be called by known partners:
custom_rules {
name = "AllowListedIPsOnly"
priority = 1
rule_type = "MatchRule"
action = "Block"
match_conditions {
match_variables {
variable_name = "RequestUri"
}
operator = "BeginsWith"
negation_condition = false
match_values = ["/api/partner/"]
}
match_conditions {
match_variables {
variable_name = "RemoteAddr"
}
operator = "IPMatch"
negation_condition = true # NOT in this list = block
match_values = [
"203.0.113.10",
"198.51.100.0/24"
]
}
}
Finding Threats in Logs
Query WAF logs to identify patterns:
AzureDiagnostics
| where ResourceType == "APPLICATIONGATEWAYS"
| where action_s == "Blocked" or action_s == "Detected"
| summarize Count = count() by clientIp_s, requestUri_s
| order by Count desc
| take 50
Look for patterns in blocked requests:
AzureDiagnostics
| where TimeGenerated > ago(24h)
| where action_s == "Matched"
| summarize
BlockCount = count(),
Paths = make_set(requestUri_s, 5)
by clientIp_s
| where BlockCount > 20
| order by BlockCount desc
Dynamic IP Blocking
For real-time response, use Logic Apps to update custom rules:
{
"name": "BlockBadActors",
"priority": 2,
"ruleType": "MatchRule",
"action": "Block",
"matchConditions": [{
"matchVariables": [{
"variableName": "RemoteAddr"
}],
"operator": "IPMatch",
"matchValues": ["DYNAMIC_LIST_FROM_THREAT_INTEL"]
}]
}
Combining Conditions
Multiple conditions are AND-ed together:
custom_rules {
name = "BlockSuspiciousPOST"
priority = 40
rule_type = "MatchRule"
action = "Block"
# Condition 1: POST request
match_conditions {
match_variables {
variable_name = "RequestMethod"
}
operator = "Equal"
match_values = ["POST"]
}
# Condition 2: AND to sensitive path
match_conditions {
match_variables {
variable_name = "RequestUri"
}
operator = "Contains"
match_values = ["/api/admin"]
}
# Condition 3: AND from external IP
match_conditions {
match_variables {
variable_name = "RemoteAddr"
}
operator = "IPMatch"
negation_condition = true
match_values = ["10.0.0.0/8"]
}
}
Need help securing your web applications? Get in touch - we help organisations implement robust security controls.