Azure Application Gateway: Reglas personalizadas de WAF v2
Resumen
El Web Application Firewall (WAF) v2 de Azure Application Gateway incluye el Core Rule Set (CRS) de OWASP para protección automática contra amenazas comunes. Sin embargo, cuando necesitas lógica de seguridad específica para tu aplicación (bloquear IPs concretas, filtrar User-Agents maliciosos, implementar rate limiting por geografía), las reglas personalizadas (custom rules) son la solución. En este post veremos cómo crear y gestionar custom rules que se evalúan antes que las managed rules, con ejemplos prácticos de bloqueo de bots, geofiltrado y rate limiting.
¿Qué son las reglas personalizadas de WAF?
Las custom rules son reglas definidas por el usuario que se evalúan antes de las managed rules (CRS). Permiten:
- Bloquear/permitir tráfico basado en condiciones específicas
- Registrar eventos sin bloquear (modo audit)
- Priorizar con valores numéricos (menor número = mayor prioridad)
- Componer lógica compleja combinando múltiples condiciones con AND/OR
Características principales
| Característica | Descripción |
|---|---|
| Prioridad | 1-100, se evalúan antes de CRS |
| Operadores | IPMatch, GeoMatch, Contains, Regex, Equal, etc. |
| Acciones | Allow, Block, Log |
| Límite | 100 custom rules por WAF policy |
| Match variables | RemoteAddr, RequestHeaders, QueryString, RequestUri, etc. |
Arquitectura de evaluación de reglas
flowchart LR
A[Request] --> B{Custom Rules
Prioridad 1-100}
B -->|Allow| C[Termina evaluación
Pasa a backend]
B -->|Block| D[Bloquea request
403 response]
B -->|Log| E{Managed Rules
CRS 3.2}
E -->|Allow| C
E -->|Block| D
E -->|Audit| F[Log event
Pasa a backend]
style B fill:#ff9900
style E fill:#0066cc
style D fill:#cc0000
Flujo de evaluación:
- Custom rules se ejecutan primero por orden de prioridad
- Si una custom rule hace
AllowoBlock, se detiene la evaluación - Si todas las custom rules pasan (o son
Log), se evalúan las managed rules (CRS)
Escenarios de uso prácticos
1. Bloquear bot malicioso por User-Agent
Caso: Un bot con User-Agent evilbot está haciendo scraping agresivo.
PowerShell:
# Variables
$rgname = "rg-waf-prod"
$policyName = "waf-policy-prod"
# Crear match variable (RequestHeaders - User-Agent)
$variable = New-AzApplicationGatewayFirewallMatchVariable `
-VariableName RequestHeaders `
-Selector User-Agent
# Crear condición de match (contiene "evilbot", case-insensitive)
$condition = New-AzApplicationGatewayFirewallCondition `
-MatchVariable $variable `
-Operator Contains `
-MatchValue "evilbot" `
-Transform Lowercase `
-NegationCondition $False
# Crear custom rule
$rule = New-AzApplicationGatewayFirewallCustomRule `
-Name blockEvilBot `
-Priority 2 `
-RuleType MatchRule `
-MatchCondition $condition `
-Action Block `
-State Enabled
# Obtener policy y agregar regla
$policy = Get-AzApplicationGatewayFirewallPolicy -Name $policyName -ResourceGroupName $rgname
$policy.CustomRules.Add($rule)
Set-AzApplicationGatewayFirewallPolicy -InputObject $policy
Resultado: Cualquier request con User-Agent: evilbot (o EvilBot, gracias a Lowercase) es bloqueado inmediatamente.
2. Geofiltrado: Permitir solo tráfico de USA
Caso: Tu aplicación solo debe ser accesible desde USA por regulaciones de compliance.
JSON (equivalente ARM template):
{
"customRules": [
{
"name": "allowOnlyUS",
"priority": 5,
"ruleType": "MatchRule",
"action": "Allow",
"state": "Enabled",
"matchConditions": [
{
"matchVariables": [
{
"variableName": "RemoteAddr"
}
],
"operator": "GeoMatch",
"negationCondition": false,
"matchValues": [
"US"
]
}
]
},
{
"name": "blockAllOthers",
"priority": 10,
"ruleType": "MatchRule",
"action": "Block",
"matchConditions": [
{
"matchVariables": [
{
"variableName": "RemoteAddr"
}
],
"operator": "GeoMatch",
"negationCondition": true,
"matchValues": [
"US"
]
}
]
}
]
}
Lógica:
- Regla priority 5: Si origen = USA →
Allow(se detiene evaluación, pasa a backend) - Regla priority 10: Si origen ≠ USA →
Block
3. Bloquear rango de IPs específico
Caso: Un rango de IPs 192.168.5.0/24 está generando ataques.
Azure CLI:
# Variables
RESOURCE_GROUP="rg-waf-prod"
POLICY_NAME="waf-policy-prod"
RULE_NAME="blockMaliciousIPs"
# Crear custom rule con IPMatch
az network application-gateway waf-policy custom-rule create \
--policy-name $POLICY_NAME \
--resource-group $RESOURCE_GROUP \
--name $RULE_NAME \
--priority 1 \
--rule-type MatchRule \
--action Block \
--state Enabled
# Agregar condición IPMatch
az network application-gateway waf-policy custom-rule match-condition add \
--match-variables RemoteAddr \
--operator IPMatch \
--values "192.168.5.0/24" \
--policy-name $POLICY_NAME \
--resource-group $RESOURCE_GROUP \
--name $RULE_NAME
4. Rate Limiting por IP + Geolocalización
Caso: Limitar tráfico a 100 req/min por IP, excluyendo IPs internas.
PowerShell:
# Crear condición que excluye IPs internas (negación)
$variable = New-AzApplicationGatewayFirewallMatchVariable -VariableName RemoteAddr
$condition = New-AzApplicationGatewayFirewallCondition `
-MatchVariable $variable `
-Operator IPMatch `
-MatchValue "10.0.0.0/8" `
-NegationCondition $True # IPs que NO sean 10.0.0.0/8
# Crear GroupBy para agrupar por ClientAddr
$groupByVariable = New-AzApplicationGatewayFirewallCustomRuleGroupByVariable -VariableName ClientAddr
$groupByUserSession = New-AzApplicationGatewayFirewallCustomRuleGroupByUserSession -GroupByVariable $groupByVariable
# Crear rate limit rule
$rateLimitRule = New-AzApplicationGatewayFirewallCustomRule `
-Name ClientIPRateLimitRule `
-Priority 90 `
-RateLimitDuration OneMin `
-RateLimitThreshold 100 `
-RuleType RateLimitRule `
-MatchCondition $condition `
-GroupByUserSession $groupByUserSession `
-Action Block `
-State Enabled
# Agregar a policy
$policy = Get-AzApplicationGatewayFirewallPolicy -Name $policyName -ResourceGroupName $rgname
$policy.CustomRules.Add($rateLimitRule)
Set-AzApplicationGatewayFirewallPolicy -InputObject $policy
Resultado: Cada IP (excepto 10.0.0.0/8) puede hacer máximo 100 requests/min. Si excede el umbral → bloqueada 1 minuto.
Operadores disponibles y sus usos
| Operador | Uso típico | Ejemplo |
|---|---|---|
| IPMatch | Bloquear/permitir rangos CIDR | 192.168.1.0/24 |
| GeoMatch | Filtrado por país | US, MX, CA |
| Contains | Buscar string en headers/URI | User-Agent contiene bot |
| Equal | Match exacto | Cookie exactamente session=abc123 |
| Regex | Patrones complejos | ^evil.*bot$ |
| BeginsWith | Prefijo de URI/header | URI empieza con /admin |
| EndsWith | Sufijo | Filename termina en .exe |
| GreaterThan | Numérico (content-length) | Body > 10MB |
Transformaciones de datos
Antes de evaluar, puedes transformar el valor:
$condition = New-AzApplicationGatewayFirewallCondition `
-MatchVariable $variable `
-Operator Contains `
-MatchValue "admin" `
-Transform Lowercase, UrlDecode, Trim
Transformaciones disponibles:
Lowercase/UppercaseUrlDecode/UrlEncodeTrim(quita espacios)RemoveNullsHtmlEntityDecode
Lógica de combinación AND/OR
Dentro de una regla (AND):
# Bloquear si IP = 192.168.5.0/24 AND User-Agent contiene "evilbot"
$rule = New-AzApplicationGatewayFirewallCustomRule `
-Name compoundRule `
-Priority 10 `
-MatchCondition $condition1, $condition2 ` # AND implícito
-Action Block
Entre reglas (OR):
# Bloquear si IP = 192.168.5.0/24 OR User-Agent contiene "evilbot"
$rule1 = New-AzApplicationGatewayFirewallCustomRule -Name blockIP ...
$rule2 = New-AzApplicationGatewayFirewallCustomRule -Name blockBot ...
# Dos reglas separadas = OR lógico
Gestión del ciclo de vida
Actualizar regla existente
# Obtener policy actual
az network application-gateway waf-policy show \
--name waf-policy-prod \
--resource-group rg-waf-prod \
--query "customRules[?name=='blockEvilBot']"
# Modificar y reemplazar
# (Editar JSON exportado y reaplicar con az network application-gateway waf-policy update)
Alternativa PowerShell:
$policy = Get-AzApplicationGatewayFirewallPolicy -Name $policyName -ResourceGroupName $rgname
$policy.CustomRules[0].Action = "Allow" # Cambiar acción de primera regla
Set-AzApplicationGatewayFirewallPolicy -InputObject $policy
Deshabilitar regla temporalmente
$policy.CustomRules[0].State = "Disabled"
Set-AzApplicationGatewayFirewallPolicy -InputObject $policy
Eliminar regla
$policy.CustomRules.RemoveAt(0) # Elimina primera regla
Set-AzApplicationGatewayFirewallPolicy -InputObject $policy
Casos de uso avanzados
Bloquear acceso a /admin excepto desde IP corporativa
Terraform:
resource "azurerm_web_application_firewall_policy" "example" {
name = "waf-policy-prod"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
custom_rules {
name = "AllowAdminFromCorp"
priority = 1
rule_type = "MatchRule"
action = "Allow"
match_conditions {
match_variables {
variable_name = "RequestUri"
}
operator = "BeginsWith"
match_values = ["/admin"]
}
match_conditions {
match_variables {
variable_name = "RemoteAddr"
}
operator = "IPMatch"
match_values = ["203.0.113.0/24"] # IP corporativa
}
}
custom_rules {
name = "BlockAdminOthers"
priority = 2
rule_type = "MatchRule"
action = "Block"
match_conditions {
match_variables {
variable_name = "RequestUri"
}
operator = "BeginsWith"
match_values = ["/admin"]
}
}
}
Regex para bloquear patrones SQL injection personalizados
$variable = New-AzApplicationGatewayFirewallMatchVariable -VariableName QueryString
$condition = New-AzApplicationGatewayFirewallCondition `
-MatchVariable $variable `
-Operator Regex `
-MatchValue "(union|select|insert|drop).*from" `
-Transform Lowercase `
-NegationCondition $False
$rule = New-AzApplicationGatewayFirewallCustomRule `
-Name blockSQLiRegex `
-Priority 3 `
-MatchCondition $condition `
-Action Block
Troubleshooting y monitorización
Logs de custom rules
Las custom rules generan eventos en Application Gateway WAF logs (categoría ApplicationGatewayFirewallLog).
Consulta Log Analytics (KQL):
AzureDiagnostics
| where Category == "ApplicationGatewayFirewallLog"
| where ruleId_s startswith "custom-" // Custom rules tienen prefix
| project TimeGenerated, clientIp_s, requestUri_s, action_s, Message
| order by TimeGenerated desc
Verificar orden de evaluación
az network application-gateway waf-policy custom-rule list \
--policy-name waf-policy-prod \
--resource-group rg-waf-prod \
--query "sort_by([].{Priority:priority, Name:name, Action:action}, &Priority)"
Output:
[
{"Priority": 1, "Name": "blockMaliciousIPs", "Action": "Block"},
{"Priority": 2, "Name": "blockEvilBot", "Action": "Block"},
{"Priority": 5, "Name": "allowOnlyUS", "Action": "Allow"}
]
Errores comunes
| Error | Causa | Solución |
|---|---|---|
| Priority conflict | Dos reglas con misma priority | Asignar valores únicos |
| Max rules exceeded | >100 custom rules | Consolidar reglas con lógica OR |
| Regex too complex | Regex consume CPU | Simplificar patrón o usar Contains |
| GeoMatch not working | IP privada/localhost | GeoMatch solo funciona para IPs públicas |
Mejores prácticas
-
Prioriza reglas Allow antes que Block: Usa priorities 1-50 para Allow, 51-100 para Block.
-
Evita regex innecesarias:
Containses más eficiente que regex simple. -
Usa transformaciones consistentes: Aplica
Lowercaseen reglas de User-Agent/cookies para evitar bypass. -
Monitoriza con Log mode primero:
Después de validar → cambia a Block.
- Documenta reglas en nombres descriptivos:
-
Rate limiting con cuidado: No uses thresholds demasiado bajos en APIs públicas (mínimo 50-100 req/min).
-
Geofiltrado + allowlist corporativa: Combina GeoMatch con IPMatch para oficinas internacionales:
# Allow: USA OR IP corporativa México
$rule1 = GeoMatch US → Allow (priority 1)
$rule2 = IPMatch 198.51.100.0/24 → Allow (priority 2)
$rule3 = Default → Block (priority 100)
Integración con SIEM
Enviar logs a Azure Sentinel:
# Habilitar diagnósticos a Log Analytics
az monitor diagnostic-settings create \
--resource /subscriptions/{sub-id}/resourceGroups/rg-waf-prod/providers/Microsoft.Network/applicationGateways/appgw-prod \
--name "send-to-sentinel" \
--logs '[{"category":"ApplicationGatewayFirewallLog","enabled":true}]' \
--workspace /subscriptions/{sub-id}/resourceGroups/rg-siem/providers/Microsoft.OperationalInsights/workspaces/sentinel-workspace
Alertas proactivas:
// Alerta: >100 blocks en 5 minutos desde misma IP
AzureDiagnostics
| where Category == "ApplicationGatewayFirewallLog"
| where action_s == "Blocked"
| summarize BlockCount = count() by clientIp_s, bin(TimeGenerated, 5m)
| where BlockCount > 100
Comparación: Custom Rules vs Managed Rules (CRS)
| Aspecto | Custom Rules | Managed Rules (CRS) |
|---|---|---|
| Orden evaluación | Primero (priority 1-100) | Después de custom rules |
| Actualización | Manual | Automática por Microsoft |
| Cobertura | Casos específicos de negocio | OWASP Top 10, amenazas conocidas |
| Complejidad | Bajo (match conditions simples) | Alto (firmas de ataque complejas) |
| Performance | Rápido (pocas reglas) | Moderado (cientos de reglas) |
| Uso recomendado | Geofiltrado, rate limiting, whitelists | Protección base contra ataques web |
Recomendación: Usa custom rules para requisitos de negocio (geolocalización, listas negras) y deja las managed rules habilitadas para protección contra ataques conocidos.
Costos y limitaciones
- Costo: Las custom rules no tienen costo adicional (incluidas en WAF v2 SKU).
- Límite máximo: 100 custom rules por policy.
- Límite de match conditions: 10 match conditions por regla.
- Match values: 600 caracteres total por match condition.
Optimización:
- Si necesitas bloquear 200 IPs, no crees 200 reglas → usa 1 regla con 200 valores en
matchValues: