Azure Application Gateway: WAF y SSL termination para aplicaciones web
Resumen
Application Gateway es un load balancer L7 con WAF integrado, SSL offloading, y routing basado en URLs. Perfecto para proteger aplicaciones web con reglas OWASP, certificados SSL centralizados, y multi-site hosting. En este post verás configuración de WAF, custom rules, SSL policies, y integration con AKS.
¿Qué es Application Gateway?
Layer 7 load balancer:
flowchart LR
Users[Users] --> AppGW[Application Gateway
+ WAF]
AppGW -->|/api/*| Backend1[Backend Pool 1
API Servers]
AppGW -->|/app/*| Backend2[Backend Pool 2
Web Servers]
AppGW -->|/admin/*| Backend3[Backend Pool 3
Admin Portal]
Backend1 --> DB[(Database)]
Backend2 --> Storage[Azure Storage]
Backend3 --> AD[Entra ID]
Características clave: - WAF: OWASP Top 10 protection, bot mitigation, custom rules - SSL offloading: Certificados centralizados, políticas TLS - URL-based routing: Diferentes backends por path - Multi-site hosting: 100+ sites en mismo gateway - Autoscaling: Scale de 0 a 125 instancias - Private Link: Conectar a backends privados
Setup básico Application Gateway
Variables y recursos previos
# Variables
RESOURCE_GROUP="rg-appgw-prod"
LOCATION="westeurope"
VNET_NAME="vnet-prod"
SUBNET_APPGW="subnet-appgw"
APPGW_NAME="appgw-prod"
PUBLIC_IP_NAME="pip-appgw"
# Crear VNet
az network vnet create \
--resource-group $RESOURCE_GROUP \
--name $VNET_NAME \
--address-prefix 10.0.0.0/16 \
--subnet-name $SUBNET_APPGW \
--subnet-prefix 10.0.1.0/24
# Public IP (Standard SKU requerido)
az network public-ip create \
--resource-group $RESOURCE_GROUP \
--name $PUBLIC_IP_NAME \
--sku Standard \
--allocation-method Static
Crear Application Gateway con WAF
# Application Gateway v2 con WAF
az network application-gateway create \
--resource-group $RESOURCE_GROUP \
--name $APPGW_NAME \
--location $LOCATION \
--sku WAF_v2 \
--capacity 2 \ # Instancias mínimas
--vnet-name $VNET_NAME \
--subnet $SUBNET_APPGW \
--public-ip-address $PUBLIC_IP_NAME \
--http-settings-cookie-based-affinity Disabled \
--frontend-port 80 \
--http-settings-port 80 \
--http-settings-protocol Http \
--priority 100 \
--servers webapp1.azurewebsites.net webapp2.azurewebsites.net # Backend pool
# Habilitar autoscaling
az network application-gateway update \
--resource-group $RESOURCE_GROUP \
--name $APPGW_NAME \
--set autoscaleConfiguration.minCapacity=2 \
--set autoscaleConfiguration.maxCapacity=10
WAF configuration
WAF policy con OWASP 3.2
# Crear WAF policy
az network application-gateway waf-policy create \
--resource-group $RESOURCE_GROUP \
--name waf-policy-prod \
--location $LOCATION
# Configurar managed rules (OWASP 3.2)
az network application-gateway waf-policy managed-rule rule-set add \
--policy-name waf-policy-prod \
--resource-group $RESOURCE_GROUP \
--type OWASP \
--version 3.2
# Bot protection rules
az network application-gateway waf-policy managed-rule rule-set add \
--policy-name waf-policy-prod \
--resource-group $RESOURCE_GROUP \
--type Microsoft_BotManagerRuleSet \
--version 1.0
# Asociar policy a Application Gateway
az network application-gateway waf-policy update \
--resource-group $RESOURCE_GROUP \
--name waf-policy-prod \
--set policySettings.mode=Prevention \ # Detection solo monitorea
--set policySettings.state=Enabled \
--set policySettings.maxRequestBodySizeInKb=128 \
--set policySettings.fileUploadLimitInMb=100
az network application-gateway update \
--resource-group $RESOURCE_GROUP \
--name $APPGW_NAME \
--set firewallPolicy.id=/subscriptions/$SUB_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Network/ApplicationGatewayWebApplicationFirewallPolicies/waf-policy-prod
Custom WAF rules
Rule 1: Rate limiting (max 100 requests/min por IP)
az network application-gateway waf-policy custom-rule create \
--policy-name waf-policy-prod \
--resource-group $RESOURCE_GROUP \
--name RateLimitRule \
--priority 100 \
--rule-type RateLimitRule \
--action Block \
--rate-limit-threshold 100 \
--rate-limit-duration 1 \
--match-conditions \
MatchVariable=RequestUri \
Operator=Contains \
MatchValue="/api/"
Rule 2: Geo-blocking (bloquear países específicos)
az network application-gateway waf-policy custom-rule create \
--policy-name waf-policy-prod \
--resource-group $RESOURCE_GROUP \
--name GeoBlockRule \
--priority 200 \
--rule-type MatchRule \
--action Block \
--match-conditions \
MatchVariable=RemoteAddr \
Operator=GeoMatch \
MatchValue="CN,RU,KP" # China, Rusia, Corea del Norte
Rule 3: Whitelist IPs corporativas
az network application-gateway waf-policy custom-rule create \
--policy-name waf-policy-prod \
--resource-group $RESOURCE_GROUP \
--name AllowCorporateIPs \
--priority 50 \ # Mayor priority = se evalúa primero
--rule-type MatchRule \
--action Allow \
--match-conditions \
MatchVariable=RemoteAddr \
Operator=IPMatch \
MatchValue="203.0.113.0/24,198.51.100.0/24" # IPs oficina
Rule 4: Bloquear User-Agents sospechosos
az network application-gateway waf-policy custom-rule create \
--policy-name waf-policy-prod \
--resource-group $RESOURCE_GROUP \
--name BlockBadUserAgents \
--priority 300 \
--rule-type MatchRule \
--action Block \
--match-conditions \
MatchVariable=RequestHeaders \
Selector=User-Agent \
Operator=Contains \
MatchValue="sqlmap,nikto,nmap,metasploit"
Exclusions (false positives)
# Excluir campo específico de WAF rules
az network application-gateway waf-policy managed-rule exclusion add \
--policy-name waf-policy-prod \
--resource-group $RESOURCE_GROUP \
--match-variable RequestHeaderNames \
--selector-match-operator Equals \
--selector "X-Custom-Header" \
--rule-set-type OWASP \
--rule-set-version 3.2
SSL/TLS configuration
Upload SSL certificate
# Crear certificado self-signed (testing)
openssl req -x509 -newkey rsa:4096 -keyout appgw-key.pem -out appgw-cert.pem -days 365 -nodes -subj "/CN=www.example.com"
# Convertir a PFX
openssl pkcs12 -export -out appgw-cert.pfx -inkey appgw-key.pem -in appgw-cert.pem -passout pass:P@ssw0rd123
# Upload certificado a Application Gateway
az network application-gateway ssl-cert create \
--resource-group $RESOURCE_GROUP \
--gateway-name $APPGW_NAME \
--name ssl-cert-example \
--cert-file appgw-cert.pfx \
--cert-password "P@ssw0rd123"
Producción: usar Key Vault
# Importar cert a Key Vault
az keyvault certificate import \
--vault-name kv-prod \
--name appgw-cert \
--file appgw-cert.pfx \
--password "P@ssw0rd123"
# Asignar Managed Identity a Application Gateway
az network application-gateway identity assign \
--resource-group $RESOURCE_GROUP \
--gateway-name $APPGW_NAME \
--identity /subscriptions/$SUB_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.ManagedIdentity/userAssignedIdentities/mi-appgw
# Dar permisos a Key Vault
az keyvault set-policy \
--name kv-prod \
--object-id $(az identity show --resource-group $RESOURCE_GROUP --name mi-appgw --query principalId -o tsv) \
--secret-permissions get list \
--certificate-permissions get list
# Referenciar cert desde Key Vault
az network application-gateway ssl-cert create \
--resource-group $RESOURCE_GROUP \
--gateway-name $APPGW_NAME \
--name ssl-cert-kv \
--key-vault-secret-id https://kv-prod.vault.azure.net/secrets/appgw-cert
HTTPS listener
# Frontend port 443
az network application-gateway frontend-port create \
--resource-group $RESOURCE_GROUP \
--gateway-name $APPGW_NAME \
--name port443 \
--port 443
# HTTPS listener
az network application-gateway http-listener create \
--resource-group $RESOURCE_GROUP \
--gateway-name $APPGW_NAME \
--name listener-https \
--frontend-port port443 \
--ssl-cert ssl-cert-example \
--host-name www.example.com # SNI
SSL policy (TLS 1.2+ only)
# Predefined policy (Modern)
az network application-gateway ssl-policy set \
--resource-group $RESOURCE_GROUP \
--gateway-name $APPGW_NAME \
--policy-type Predefined \
--policy-name AppGwSslPolicy20220101 # Solo TLS 1.2+
# Custom policy
az network application-gateway ssl-policy set \
--resource-group $RESOURCE_GROUP \
--gateway-name $APPGW_NAME \
--policy-type Custom \
--min-protocol-version TLSv1_2 \
--cipher-suites \
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 \
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 \
TLS_DHE_RSA_WITH_AES_256_GCM_SHA384
HTTP → HTTPS redirect
# Redirect configuration
az network application-gateway redirect-config create \
--resource-group $RESOURCE_GROUP \
--gateway-name $APPGW_NAME \
--name redirect-http-to-https \
--type Permanent \
--target-listener listener-https \
--include-path true \
--include-query-string true
# Routing rule HTTP → redirect
az network application-gateway rule create \
--resource-group $RESOURCE_GROUP \
--gateway-name $APPGW_NAME \
--name rule-http-redirect \
--http-listener listener-http \
--rule-type Basic \
--redirect-config redirect-http-to-https \
--priority 200
URL-based routing
Backend pools
# Backend pool: API servers
az network application-gateway address-pool create \
--resource-group $RESOURCE_GROUP \
--gateway-name $APPGW_NAME \
--name pool-api \
--servers api1.example.com api2.example.com
# Backend pool: Web servers
az network application-gateway address-pool create \
--resource-group $RESOURCE_GROUP \
--gateway-name $APPGW_NAME \
--name pool-web \
--servers web1.example.com web2.example.com
# Backend pool: Admin portal
az network application-gateway address-pool create \
--resource-group $RESOURCE_GROUP \
--gateway-name $APPGW_NAME \
--name pool-admin \
--servers admin.example.com
HTTP settings
# HTTP settings para API (timeout 60s)
az network application-gateway http-settings create \
--resource-group $RESOURCE_GROUP \
--gateway-name $APPGW_NAME \
--name http-settings-api \
--port 443 \
--protocol Https \
--cookie-based-affinity Disabled \
--timeout 60 \
--probe probe-api
# HTTP settings para Web (timeout 30s, session affinity)
az network application-gateway http-settings create \
--resource-group $RESOURCE_GROUP \
--gateway-name $APPGW_NAME \
--name http-settings-web \
--port 443 \
--protocol Https \
--cookie-based-affinity Enabled \
--timeout 30
URL path map
# Path-based rule
az network application-gateway url-path-map create \
--resource-group $RESOURCE_GROUP \
--gateway-name $APPGW_NAME \
--name url-path-map \
--paths /api/* \
--address-pool pool-api \
--http-settings http-settings-api \
--default-address-pool pool-web \
--default-http-settings http-settings-web
# Agregar otro path
az network application-gateway url-path-map rule create \
--resource-group $RESOURCE_GROUP \
--gateway-name $APPGW_NAME \
--path-map-name url-path-map \
--name rule-admin \
--paths /admin/* \
--address-pool pool-admin \
--http-settings http-settings-web
# Routing rule con path-based
az network application-gateway rule create \
--resource-group $RESOURCE_GROUP \
--gateway-name $APPGW_NAME \
--name rule-path-based \
--http-listener listener-https \
--rule-type PathBasedRouting \
--url-path-map url-path-map \
--priority 100
Health probes
Custom health probe
# Probe para API
az network application-gateway probe create \
--resource-group $RESOURCE_GROUP \
--gateway-name $APPGW_NAME \
--name probe-api \
--protocol Https \
--host api.example.com \
--path /health \
--interval 30 \
--timeout 30 \
--threshold 3 \ # 3 fallos consecutivos = unhealthy
--match-status-codes 200-399
# Probe con body match
az network application-gateway probe update \
--resource-group $RESOURCE_GROUP \
--gateway-name $APPGW_NAME \
--name probe-api \
--match-body "healthy" # Response debe contener "healthy"
Health endpoint en backend:
// ASP.NET Core
app.MapGet("/health", () =>
{
var dbHealthy = CheckDatabase();
var cacheHealthy = CheckRedis();
if (!dbHealthy || !cacheHealthy)
return Results.StatusCode(503);
return Results.Ok(new { status = "healthy", timestamp = DateTime.UtcNow });
});
Integration con AKS (AGIC)
Application Gateway Ingress Controller
# Habilitar AGIC addon en AKS
az aks enable-addons \
--resource-group $RESOURCE_GROUP \
--name aks-prod \
--addon ingress-appgw \
--appgw-id /subscriptions/$SUB_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Network/applicationGateways/$APPGW_NAME
# Verificar instalación
kubectl get pods -n kube-system -l app=ingress-appgw
Ingress resource con AGIC:
# ingress-api.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-ingress
namespace: prod
annotations:
kubernetes.io/ingress.class: azure/application-gateway
appgw.ingress.kubernetes.io/ssl-redirect: "true"
appgw.ingress.kubernetes.io/backend-protocol: "https"
appgw.ingress.kubernetes.io/health-probe-path: "/health"
appgw.ingress.kubernetes.io/request-timeout: "60"
spec:
tls:
- hosts:
- api.example.com
secretName: tls-secret # K8s secret con certificado
rules:
- host: api.example.com
http:
paths:
- path: /api/*
pathType: Prefix
backend:
service:
name: api-service
port:
number: 443
Annotations útiles:
annotations:
# WAF
appgw.ingress.kubernetes.io/waf-policy-for-path: "/subscriptions/.../waf-policy-prod"
# Connection draining
appgw.ingress.kubernetes.io/connection-draining: "true"
appgw.ingress.kubernetes.io/connection-draining-timeout: "30"
# Cookie affinity
appgw.ingress.kubernetes.io/cookie-based-affinity: "true"
# Custom health probe
appgw.ingress.kubernetes.io/health-probe-status-codes: "200-399"
appgw.ingress.kubernetes.io/health-probe-interval: "30"
# Private IP (internal load balancer)
appgw.ingress.kubernetes.io/use-private-ip: "true"
Monitoring y logs
Diagnostic settings
# Habilitar logs
az monitor diagnostic-settings create \
--name appgw-diagnostics \
--resource /subscriptions/$SUB_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Network/applicationGateways/$APPGW_NAME \
--logs '[
{"category":"ApplicationGatewayAccessLog","enabled":true},
{"category":"ApplicationGatewayPerformanceLog","enabled":true},
{"category":"ApplicationGatewayFirewallLog","enabled":true}
]' \
--metrics '[{"category":"AllMetrics","enabled":true}]' \
--workspace /subscriptions/$SUB_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.OperationalInsights/workspaces/law-prod
KQL queries útiles
// Top 10 URLs más lentas
AzureDiagnostics
| where ResourceType == "APPLICATIONGATEWAYS"
| where Category == "ApplicationGatewayAccessLog"
| summarize AvgResponseTime = avg(timeTaken_d) by requestUri_s
| top 10 by AvgResponseTime desc
// WAF blocked requests
AzureDiagnostics
| where ResourceType == "APPLICATIONGATEWAYS"
| where Category == "ApplicationGatewayFirewallLog"
| where action_s == "Blocked"
| summarize Count = count() by ruleId_s, clientIp_s, requestUri_s
| order by Count desc
// HTTP 5xx errors
AzureDiagnostics
| where Category == "ApplicationGatewayAccessLog"
| where httpStatus_d >= 500
| project TimeGenerated, httpStatus_d, requestUri_s, backendSettingName_s, clientIp_s
| order by TimeGenerated desc
Alertas
# Alerta: unhealthy backend
az monitor metrics alert create \
--name alert-appgw-unhealthy-backend \
--resource-group $RESOURCE_GROUP \
--scopes /subscriptions/$SUB_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Network/applicationGateways/$APPGW_NAME \
--condition "avg UnhealthyHostCount > 0" \
--window-size 5m \
--evaluation-frequency 1m \
--action $ACTION_GROUP_ID \
--severity 1
# Alerta: WAF blocking spike
az monitor metrics alert create \
--name alert-appgw-waf-blocks \
--resource-group $RESOURCE_GROUP \
--scopes /subscriptions/$SUB_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Network/applicationGateways/$APPGW_NAME \
--condition "total BlockedCount > 100" \
--window-size 5m \
--action $ACTION_GROUP_ID
Buenas prácticas
Security: - ✅ WAF en Prevention mode (no solo Detection) - ✅ Rate limiting custom rules (proteger APIs) - ✅ Geo-blocking si app es regional - ✅ SSL certificates en Key Vault (auto-renewal) - ✅ TLS 1.2+ only (disable TLS 1.0/1.1)
Performance: - ✅ Autoscaling habilitado (min 2, max 10+) - ✅ Connection draining (evitar 502 en deployments) - ✅ Custom health probes (no default) - ✅ HTTP/2 enabled
Resiliency: - ✅ Multiple backend pools (redundancia) - ✅ Health probes con threshold 3 - ✅ Timeout adecuado por backend (API: 60s, Web: 30s)
Cost optimization: - ✅ WAF_v2 vs Standard_v2 (solo si necesitas WAF) - ✅ Autoscaling scale-to-zero en dev/test - ✅ Reservations para producción (30-40% descuento)
Troubleshooting
Problema: 502 Bad Gateway
Causas comunes: 1. Backend unhealthy (health probe failing) 2. NSG bloqueando tráfico 3. Backend timeout
# Verificar backend health
az network application-gateway show-backend-health \
--resource-group $RESOURCE_GROUP \
--name $APPGW_NAME
# Ver logs
az monitor activity-log list \
--resource-group $RESOURCE_GROUP \
--resource-id /subscriptions/$SUB_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Network/applicationGateways/$APPGW_NAME \
--start-time 2025-05-15T00:00:00Z
Problema: WAF bloqueando tráfico legítimo
// Identificar regla problemática
AzureDiagnostics
| where Category == "ApplicationGatewayFirewallLog"
| where action_s == "Blocked"
| where clientIp_s == "<IP-legitima>"
| summarize Count = count() by ruleId_s, message_s
Fix: Agregar exclusion o deshabilitar regla específica
Costes
Pricing Application Gateway:
WAF_v2:
- Gateway: $0.443/hora (~$320/mes)
- Capacity Unit (CU): $0.008/hora por CU
- Data processed: $0.008/GB
Ejemplo producción:
- Gateway base: $320/mes
- 5 CUs promedio: $288/mes (5 × 0.008 × 720h)
- 1 TB data: $8/mes
Total: ~$616/mes
Con autoscaling (2-10 instances):
- Min (2 CUs): ~$435/mes
- Max (20 CUs): ~$1,476/mes
- Promedio: ~$750/mes