Back to Blog
Azure
4 min read

WAF Custom Rules for Blocking Bots and Scanners

AzureWAFSecurityApplication GatewayBots

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.

Need help with your Azure environment?

Get in touch for a free consultation.

Get in Touch