El plan de respuesta de 04-04 terminaba siempre en el mismo sitio. Marta identifica el patrón del
ataque —peticiones a /buscar con parámetros aleatorios, desde 8.400 direcciones de 61 países, con un
agente de usuario falsificado idéntico en el 91 % de los casos— y entonces necesita una herramienta
capaz de actuar sobre eso. No sobre una IP. No sobre un puerto. Sobre el contenido de la petición
HTTP.
Ni sg-mercadofresco-tienda ni la NACL de las subredes públicas pueden hacerlo, y no por falta de
funciones: es que trabajan en otra capa. Un grupo de seguridad ve un paquete TCP hacia el puerto 443 y
no tiene forma de saber si dentro va una consulta de un cliente de Getafe o una inyección SQL. Shield,
por su parte, trabaja con volumen y con paquetes malformados: un ataque compuesto por peticiones HTTP
perfectamente válidas le resulta invisible.
AWS WAF (Web Application Firewall) es el cortafuegos que sí mira dentro. Inspecciona el método,
la ruta, la cadena de consulta, las cabeceras, las cookies y el cuerpo de cada petición, y decide.
Esta lección lo monta delante de la distribución E2QWERTY123ABC y del alb-mercadofresco-tienda, y
cierra con ello el módulo de seguridad.
Advertencia. El enfoque de esta lección es estrictamente defensivo: proteger tu propia aplicación. Los ejemplos son didácticos y están simplificados. Cualquier configuración de WAF que vaya a aplicarse a un entorno real con datos de clientes —especialmente si intervienen el RGPD o PCI DSS— debe ser revisada por un profesional de seguridad o de cumplimiento antes de ponerse en producción. Una regla mal calibrada bloquea clientes legítimos y puede costar más que el ataque del que pretende protegerte; el procedimiento de despliegue que se describe aquí no es una recomendación opcional.
Contenido
- Qué ve WAF que un grupo de seguridad no puede ver
- Dónde se asocia una Web ACL
- Componentes: Web ACL, reglas y grupos de reglas
- Unidades de capacidad (WCU)
- Acciones y prioridad de evaluación
- El recorrido de una petición
- Declaraciones de coincidencia
- Transformaciones de texto
- Grupos de reglas gestionadas por AWS
- El método de despliegue:
Countprimero,Blockdespués - Reglas basadas en tasa
- La Web ACL de MercadoFresco, completa
- Asociar la Web ACL a CloudFront y al ALB
- Registros de WAF y su análisis
- Métricas y muestras de peticiones bloqueadas
- Falsos positivos: diagnóstico y exclusiones
- Coste y cálculo para MercadoFresco
- Limpieza
- La postura de seguridad de MercadoFresco
- Lo que falta: nadie está mirando
Qué ve WAF que un grupo de seguridad no puede ver
| Grupo de seguridad | NACL | AWS WAF | |
|---|---|---|---|
| Capa OSI | 3/4 | 3/4 | 7 (aplicación) |
| Inspecciona | IP, protocolo, puerto | IP, protocolo, puerto | Método, ruta, cabeceras, cookies, cuerpo, consulta |
| Estado | Con estado | Sin estado | Con estado por petición |
| Se aplica a | ENI (instancia, ALB, RDS) | Subred | CloudFront, ALB, API Gateway, AppSync, Cognito |
| Detecta inyección SQL | No | No | Sí |
| Bloquea por país | No | No | Sí |
| Limita peticiones por IP | No | No | Sí |
| Distingue navegador de bot | No | No | Sí |
| Coste | Gratis | Gratis | 5 USD/mes + reglas + peticiones |
Un ejemplo concreto que resume la diferencia. Esta petición:
Para sg-mercadofresco-alb es una conexión TCP al puerto 443 desde una IP cualquiera: la permite,
porque es exactamente lo que debe permitir. Para WAF es una petición con un patrón de inyección SQL en
la cadena de consulta y una herramienta de ataque declarada en el agente de usuario: la bloquea.
Las tres herramientas son complementarias y ninguna sustituye a otra:
| Amenaza | Herramienta |
|---|---|
| Acceso al puerto 5432 de la base de datos desde internet | Grupo de seguridad (03-02) |
| Inundación volumétrica de 200 Gbps | Shield (04-04) |
| Inyección SQL, XSS, scripts maliciosos | WAF |
Inundación HTTP contra /buscar |
WAF con regla por tasa |
| Credenciales robadas y usadas correctamente | IAM (04-01), no hay cortafuegos que ayude |
Dónde se asocia una Web ACL
| Servicio | Ámbito | Notas |
|---|---|---|
| CloudFront | CLOUDFRONT |
La Web ACL debe crearse en us-east-1 |
| Application Load Balancer | REGIONAL |
En la región del ALB |
| API Gateway (REST) | REGIONAL |
Por etapa |
| AppSync | REGIONAL |
GraphQL |
| Cognito | REGIONAL |
Grupos de usuarios |
| App Runner, Verified Access | REGIONAL |
Menos habituales |
La primera fila esconde la trampa que más tiempo hace perder: una Web ACL para CloudFront se crea en
us-east-1 con --scope CLOUDFRONT, sin importar dónde esté el resto de tu infraestructura. Es la
misma regla que ya vimos con los certificados de ACM en 03-04 y con las métricas de CloudFront en
04-04. Una Web ACL regional creada en eu-west-1 no se puede asociar a una distribución.
Para MercadoFresco montamos dos Web ACL:
| Web ACL | Ámbito | Región | Protege |
|---|---|---|---|
waf-mercadofresco-cdn |
CLOUDFRONT |
us-east-1 |
Distribución E2QWERTY123ABC |
waf-mercadofresco-alb |
REGIONAL |
eu-west-1 |
alb-mercadofresco-tienda |
¿Por qué dos, si todo el tráfico pasa por CloudFront? Por defensa en profundidad. La del ALB es la red de seguridad para el caso de que alguien consiga saltarse el borde, y protege también el tráfico interno o de pruebas que llegue directamente. Es el mismo razonamiento por el que en 04-04 cerramos el ALB con la cabecera verificada además de con la lista de prefijos.
Componentes: Web ACL, reglas y grupos de reglas
flowchart TD
A["Web ACL<br/>waf-mercadofresco-cdn"] --> B["Regla 1 - prioridad 0<br/>IP set: bloqueo permanente"]
A --> C["Regla 2 - prioridad 10<br/>Grupo gestionado:<br/>AmazonIpReputationList"]
A --> D["Regla 3 - prioridad 20<br/>Grupo gestionado:<br/>CommonRuleSet"]
A --> E["Regla 4 - prioridad 30<br/>Grupo gestionado:<br/>SQLiRuleSet"]
A --> F["Regla 5 - prioridad 40<br/>Por tasa: /login"]
A --> G["Regla 6 - prioridad 50<br/>Por tasa: /api/pedidos"]
A --> H["Accion por defecto:<br/>ALLOW"]
Los cuatro conceptos:
- Web ACL (Web Access Control List): el contenedor. Tiene una lista ordenada de reglas y una acción por defecto que se aplica a lo que no coincide con ninguna.
- Regla: una declaración de coincidencia más una acción. Tiene una prioridad numérica.
- Grupo de reglas: un conjunto reutilizable de reglas. Pueden ser gestionados por AWS, gestionados por terceros (del Marketplace) o propios.
- IP set y regex pattern set: listas reutilizables de direcciones o de expresiones regulares que las reglas referencian por ARN.
La acción por defecto define el modelo:
| Acción por defecto | Modelo | Cuándo |
|---|---|---|
Allow |
Lista negra: se permite todo salvo lo que coincide con una regla | Sitios públicos como una tienda |
Block |
Lista blanca: se bloquea todo salvo lo explícitamente permitido | Paneles internos, APIs privadas |
MercadoFresco usa Allow por defecto en la tienda pública. Para admin.mercadofresco.example, el
subdominio de administración que creamos en 03-05, el modelo correcto sería Block por defecto con
una regla que solo permita la IP de la oficina 192.168.10.0/24 y su rango de salida público.
Unidades de capacidad (WCU)
Cada regla consume WCU (Web ACL Capacity Units), una medida del coste de cómputo de evaluarla. Una Web ACL tiene un límite de 1.500 WCU por defecto (ampliable a 5.000 solicitándolo).
| Elemento | WCU aproximadas |
|---|---|
| Coincidencia de IP set | 1 |
| Coincidencia de cadena simple | 1-5 |
| Cada transformación de texto | +10 por cada una |
| Expresión regular | 25-35 |
| Coincidencia geográfica | 1 |
| Regla por tasa | 2 |
AWSManagedRulesCommonRuleSet |
700 |
AWSManagedRulesKnownBadInputsRuleSet |
200 |
AWSManagedRulesSQLiRuleSet |
200 |
AWSManagedRulesLinuxRuleSet |
200 |
AWSManagedRulesAmazonIpReputationList |
25 |
AWSManagedRulesAnonymousIpList |
50 |
AWSManagedRulesBotControlRuleSet |
50 |
El presupuesto se agota antes de lo que parece: CommonRuleSet solo consume casi la mitad. Conviene
planificarlo:
aws wafv2 describe-managed-rule-group \
--vendor-name AWS --name AWSManagedRulesCommonRuleSet \
--scope CLOUDFRONT --region us-east-1 \
--query '{Capacidad:Capacity, Reglas:Rules[].Name}' \
--profile mercadofresco-devY consultar cuánto llevas consumido:
aws wafv2 get-web-acl --name waf-mercadofresco-cdn --scope CLOUDFRONT \
--id <id> --region us-east-1 --query 'WebACL.Capacity' --profile mercadofresco-devAcciones y prioridad de evaluación
| Acción | Qué hace | Coste para el cliente |
|---|---|---|
Allow |
Permite la petición y detiene la evaluación | Ninguno |
Block |
Rechaza con 403 (personalizable) y detiene la evaluación | Petición perdida |
Count |
Cuenta y continúa evaluando. No bloquea nada | Ninguno |
CAPTCHA |
Muestra un desafío visual; si se resuelve, emite un token válido unos minutos | Fricción alta |
Challenge |
Desafío silencioso de JavaScript; el navegador lo resuelve solo | Fricción casi nula |
Dos comportamientos que hay que tener muy claros:
AllowyBlockson terminales. En cuanto una regla coincide con una de esas dos acciones, la evaluación se detiene y no se miran las reglas siguientes. Por eso la prioridad importa tanto.Countnunca es terminal. Registra la coincidencia y sigue. Es la base del método de despliegue seguro.
Sobre CAPTCHA frente a Challenge: Challenge es casi siempre la opción correcta. Verifica que hay
un navegador real ejecutando JavaScript sin molestar al usuario, filtra la práctica totalidad de los
bots simples y no daña la conversión. CAPTCHA resérvalo para acciones críticas —/login tras varios
intentos fallidos, o el paso final del pago si detectas fraude— porque cada CAPTCHA mostrado a un
cliente real es un porcentaje de ventas perdidas.
La prioridad se evalúa de menor a mayor número. No tienen que ser consecutivos: usar 0, 10, 20, 30 en lugar de 0, 1, 2, 3 permite insertar reglas después sin renumerar todo.
El recorrido de una petición
flowchart TD
A["Peticion HTTPS de un cliente"] --> B{"Prioridad 0<br/>IP en lista de bloqueo?"}
B -->|"Si"| Z["BLOCK 403 - fin"]
B -->|"No"| C{"Prioridad 10<br/>IP con mala reputacion?"}
C -->|"Si"| Z
C -->|"No"| D{"Prioridad 20<br/>CommonRuleSet: XSS, rutas,<br/>tamano, agentes malos?"}
D -->|"Si"| Z
D -->|"No"| E{"Prioridad 30<br/>Inyeccion SQL?"}
E -->|"Si"| Z
E -->|"No"| F{"Prioridad 40<br/>Mas de 100 peticiones a /login<br/>en 5 min desde esta IP?"}
F -->|"Si"| Y["CAPTCHA"]
F -->|"No"| G{"Prioridad 50<br/>Mas de 2000 a /api/pedidos<br/>en 5 min?"}
G -->|"Si"| Z
G -->|"No"| H["Accion por defecto: ALLOW"]
H --> I["CloudFront sirve<br/>desde cache u origen"]
Fíjate en el orden, que no es casual:
- Lo más barato y más seguro primero: comprobar una IP en un conjunto cuesta 1 WCU y no tiene falsos positivos.
- Las reglas gestionadas en medio: caras en WCU pero muy eficaces.
- Las reglas por tasa al final: solo tiene sentido contar peticiones que ya han superado todos los filtros anteriores.
Declaraciones de coincidencia
| Declaración | Qué compara | Ejemplo de uso |
|---|---|---|
| ByteMatch | Una cadena en un componente de la petición | El agente de usuario contiene sqlmap |
| RegexPatternSet | Una expresión regular | Rutas que encajan con ^/admin/.* |
| SizeConstraint | Tamaño de un componente | Cuerpo mayor de 8 KB en /api/pedidos |
| GeoMatch | País de origen (por GeoIP) | Bloquear países sin clientes |
| IPSet | IP o rango en una lista | Permitir siempre la oficina |
| SqliMatch | Patrones de inyección SQL | Cadena de consulta y cuerpo |
| XssMatch | Patrones de scripting entre sitios | Formularios |
| LabelMatch | Etiqueta puesta por una regla anterior | Combinar reglas gestionadas con lógica propia |
| RateBased | Peticiones por ventana de tiempo | Proteger /login |
| And / Or / Not | Combinación lógica | «España y ruta /admin» |
Los componentes de la petición que se pueden inspeccionar: UriPath, QueryString,
SingleHeader, AllHeaders, Cookies, Method, Body (primeros 8 KB en el ALB, hasta 64 KB en
CloudFront con configuración), JsonBody (con análisis de JSON) y SingleQueryArgument.
Ejemplo de una regla propia que bloquea el acceso a /admin desde fuera de España:
{
"Name": "AdminSoloDesdeEspana",
"Priority": 5,
"Statement": {
"AndStatement": {
"Statements": [
{
"ByteMatchStatement": {
"SearchString": "/admin",
"FieldToMatch": { "UriPath": {} },
"TextTransformations": [
{ "Priority": 0, "Type": "LOWERCASE" },
{ "Priority": 1, "Type": "URL_DECODE" }
],
"PositionalConstraint": "STARTS_WITH"
}
},
{
"NotStatement": {
"Statement": {
"GeoMatchStatement": { "CountryCodes": ["ES"] }
}
}
}
]
}
},
"Action": { "Block": {} },
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "AdminSoloDesdeEspana"
}
}AndStatement exige que se cumplan las dos condiciones: la ruta empieza por /admin y el país no
es España. PositionalConstraint admite EXACTLY, STARTS_WITH, ENDS_WITH, CONTAINS y
CONTAINS_WORD.
Un aviso sobre GeoMatchStatement: la geolocalización por IP no es infalible y una VPN la esquiva sin
esfuerzo. Sirve para reducir ruido de fondo, no como control de acceso serio. Y ojo con bloquear
países: un cliente español de vacaciones en Francia dejaría de poder comprar.
Transformaciones de texto
Son imprescindibles y se olvidan constantemente. Una regla que busca la cadena <script> no
coincide con ninguna de estas variantes, que hacen exactamente lo mismo:
| Variante | Técnica |
|---|---|
<SCRIPT> |
Mayúsculas |
%3Cscript%3E |
Codificación de URL |
<scr\x00ipt> |
Byte nulo insertado |
<script > |
Espacios adicionales |
<script> |
Entidades HTML |
<scr<script>ipt> |
Anidamiento |
Las transformaciones de texto normalizan el valor antes de compararlo:
| Transformación | Qué hace |
|---|---|
NONE |
Nada |
LOWERCASE |
Todo a minúsculas |
URL_DECODE |
Decodifica %3C a < |
HTML_ENTITY_DECODE |
Decodifica < a < |
COMPRESS_WHITE_SPACE |
Colapsa espacios múltiples en uno |
REMOVE_NULLS |
Elimina bytes nulos |
CMD_LINE |
Normaliza sintaxis de línea de comandos |
BASE64_DECODE |
Decodifica base64 |
NORMALIZE_PATH |
Resuelve ../ y // en rutas |
Se aplican en orden de Priority y se pueden encadenar. La combinación defensiva estándar:
"TextTransformations": [
{ "Priority": 0, "Type": "URL_DECODE" },
{ "Priority": 1, "Type": "HTML_ENTITY_DECODE" },
{ "Priority": 2, "Type": "LOWERCASE" },
{ "Priority": 3, "Type": "REMOVE_NULLS" },
{ "Priority": 4, "Type": "COMPRESS_WHITE_SPACE" }
]Una regla sin transformaciones de texto es una regla que se esquiva escribiendo en mayúsculas. Cada transformación cuesta 10 WCU, y ese es el precio de que la regla sirva para algo.
Los grupos de reglas gestionadas por AWS ya incluyen las transformaciones adecuadas: es una de las razones para empezar por ellos.
Grupos de reglas gestionadas por AWS
AWS mantiene y actualiza estos conjuntos. La mayoría son gratuitos: solo se pagan las WCU que consumen dentro de tu Web ACL, no un cargo aparte.
| Grupo | WCU | Coste | Qué protege | ¿Para MercadoFresco? |
|---|---|---|---|---|
AWSManagedRulesCommonRuleSet |
700 | Gratis | Base OWASP: XSS, rutas maliciosas, tamaños anómalos, agentes de usuario vacíos | Sí, esencial |
AWSManagedRulesKnownBadInputsRuleSet |
200 | Gratis | Entradas de vulnerabilidades conocidas (Log4Shell, deserialización) | Sí, esencial |
AWSManagedRulesSQLiRuleSet |
200 | Gratis | Inyección SQL en consulta, cuerpo y cookies | Sí: hay PostgreSQL detrás |
AWSManagedRulesLinuxRuleSet |
200 | Gratis | LFI, inclusión de /etc/passwd, ejecución de comandos |
Sí: las instancias son Linux |
AWSManagedRulesUnixRuleSet |
100 | Gratis | Comandos de shell | Redundante con el anterior |
AWSManagedRulesWindowsRuleSet |
200 | Gratis | PowerShell, comandos de Windows | No: no hay Windows |
AWSManagedRulesPHPRuleSet |
100 | Gratis | Inyección de PHP | Solo si la tienda usa PHP |
AWSManagedRulesWordPressRuleSet |
100 | Gratis | Vulnerabilidades de WordPress | No |
AWSManagedRulesAmazonIpReputationList |
25 | Gratis | IP con actividad maliciosa conocida y nodos de botnets | Sí, barata y muy eficaz |
AWSManagedRulesAnonymousIpList |
50 | Gratis | VPN, Tor, proxies, salidas de nubes | Con cuidado: hay clientes legítimos con VPN |
AWSManagedRulesBotControlRuleSet |
50 | 10 USD/mes + 1 USD/millón | Clasifica bots: buscadores, rastreadores, herramientas | Evaluar más adelante |
AWSManagedRulesATPRuleSet |
50 | 10 USD/mes + 1 USD/1.000 intentos | Account Takeover Prevention: relleno de credenciales, credenciales filtradas | Interesante para /login |
AWSManagedRulesACFPRuleSet |
50 | Pago | Prevención de registro fraudulento de cuentas | No por ahora |
Dos matices sobre los de pago:
- Bot Control tiene dos niveles: el común, que identifica bots que se declaran (buscadores, monitorización), y el dirigido, que detecta bots que se hacen pasar por navegadores usando huellas digitales y desafíos. El dirigido es notablemente más caro y solo compensa con un problema real de scraping o de reventa.
- ATP comprueba las credenciales de cada intento de acceso contra una base de datos de credenciales filtradas, y detecta patrones de relleno. Para MercadoFresco, con cuentas de clientes que guardan direcciones de reparto, es la primera ampliación a considerar cuando haya presupuesto.
Empieza siempre por los gratuitos. CommonRuleSet + KnownBadInputs + SQLi + IpReputationList
son 1.125 WCU, gratis, y cubren la inmensa mayoría de lo que verás.
El método de despliegue: Count primero, Block después
Este apartado es el más importante de la lección. Activar CommonRuleSet en modo Block un viernes
por la tarde y descubrir que una de sus reglas bloquea el formulario de pedido es un desastre
autoinfligido peor que cualquier ataque.
El procedimiento correcto tiene cuatro fases:
flowchart TD
A["Fase 1: crear la Web ACL con TODAS<br/>las reglas en Count y accion por defecto Allow"] --> B["Fase 2: dejarla 7-14 dias,<br/>cubriendo al menos dos viernes"]
B --> C["Fase 3: analizar los registros<br/>Que reglas cuentan?<br/>Contra que peticiones reales?"]
C --> D{"Hay falsos<br/>positivos?"}
D -->|"Si"| E["Anadir exclusiones de reglas<br/>concretas o afinar el alcance"]
E --> C
D -->|"No"| F["Fase 4: pasar a Block<br/>de una en una, empezando<br/>por la mas segura"]
F --> G["Vigilar 24-48 h<br/>tras cada cambio"]
G --> H["Web ACL en produccion"]
Por qué cada fase:
- Fase 1.
Countregistra la coincidencia y deja pasar la petición. Riesgo cero: si te equivocas, no pasa nada. - Fase 2. Dos semanas incluyen los dos picos de viernes y, con suerte, alguna campaña. Un despliegue basado en tres días de tráfico de martes no ve los casos raros, y los casos raros son precisamente los falsos positivos.
- Fase 3. Los registros dicen exactamente qué regla habría bloqueado qué petición real, con su URI, sus cabeceras y su IP. Esa es la información que no se puede adivinar.
- Fase 4. De una en una. Si algo se rompe, sabes exactamente cuál fue.
Orden recomendado para pasar a Block, de menos a más riesgo de falso positivo:
| Orden | Regla | Riesgo |
|---|---|---|
| 1 | IP set de bloqueo propio | Nulo: lo has puesto tú |
| 2 | AmazonIpReputationList |
Muy bajo |
| 3 | SQLiRuleSet |
Bajo, si no tienes formularios con SQL legítimo |
| 4 | KnownBadInputsRuleSet |
Bajo |
| 5 | Reglas por tasa | Medio: hay que calibrar el umbral |
| 6 | LinuxRuleSet |
Medio |
| 7 | CommonRuleSet |
El más alto: es el más amplio |
| 8 | AnonymousIpList |
Alto: bloquea clientes con VPN |
Una regla en Count dentro de un grupo gestionado se configura con RuleActionOverrides, que permite
pasar a Count reglas individuales del grupo sin desactivarlo entero:
{
"Name": "ConjuntoComun",
"Priority": 20,
"Statement": {
"ManagedRuleGroupStatement": {
"VendorName": "AWS",
"Name": "AWSManagedRulesCommonRuleSet",
"RuleActionOverrides": [
{ "Name": "SizeRestrictions_BODY", "ActionToUse": { "Count": {} } },
{ "Name": "NoUserAgent_HEADER", "ActionToUse": { "Count": {} } }
]
}
},
"OverrideAction": { "None": {} },
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "ConjuntoComun"
}
}Dos campos que se confunden siempre:
OverrideAction(solo en grupos de reglas):{"None": {}}respeta las acciones del grupo;{"Count": {}}pone todo el grupo en modo cuenta. Durante la fase 1 se usaCount.RuleActionOverrides: cambia la acción de reglas concretas dentro del grupo. Es la herramienta fina, la que se usa en la fase 3 para neutralizar un falso positivo sin renunciar al resto del grupo.
Reglas basadas en tasa
Cuentan las peticiones que coinciden con una condición dentro de una ventana de tiempo y actúan cuando se supera un umbral. Son la respuesta directa al ataque de 04-04.
| Parámetro | Valores | Comentario |
|---|---|---|
Limit |
10 a 2.000.000.000 | Peticiones por ventana |
EvaluationWindowSec |
60, 120, 300, 600 | Por defecto 300 (5 minutos) |
AggregateKeyType |
IP, FORWARDED_IP, CUSTOM_KEY, CONSTANT |
Cómo se agrupa el recuento |
ScopeDownStatement |
Cualquier declaración | A qué peticiones se aplica el recuento |
AggregateKeyType merece explicación:
IP: por dirección de origen. Lo habitual.FORWARDED_IP: usaX-Forwarded-For. Imprescindible si el WAF está en el ALB detrás de CloudFront, porque si no, todas las peticiones parecerán venir de las IP de CloudFront y contarán como una sola fuente.CUSTOM_KEY: agrupa por cookie de sesión, cabecera, parámetro de consulta o combinación. Permite limitar «por cuenta de usuario» en lugar de «por IP», mucho más preciso frente a atacantes distribuidos.CONSTANT: cuenta todo junto, sin agrupar. Sirve para poner un techo global a una ruta cara.
ScopeDownStatement es la pieza que la hace útil: sin él, la regla contaría todas las peticiones
del sitio, y un cliente que navega por el catálogo generaría cientos de peticiones legítimas. Con él,
solo cuentan las que van a la ruta protegida.
Protección de /login con CAPTCHA en lugar de bloqueo:
{
"Name": "LimiteLogin",
"Priority": 40,
"Statement": {
"RateBasedStatement": {
"Limit": 100,
"EvaluationWindowSec": 300,
"AggregateKeyType": "IP",
"ScopeDownStatement": {
"ByteMatchStatement": {
"SearchString": "/login",
"FieldToMatch": { "UriPath": {} },
"TextTransformations": [{ "Priority": 0, "Type": "LOWERCASE" }],
"PositionalConstraint": "STARTS_WITH"
}
}
}
},
"Action": { "Captcha": {} },
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "LimiteLogin"
}
}100 intentos de acceso en 5 minutos desde una misma IP es muchísimo para una persona y poco para un
ataque de relleno de credenciales. Se elige Captcha y no Block deliberadamente: una oficina
con NAT compartida puede superar el umbral legítimamente, y un CAPTCHA la deja pasar mientras que un
bloqueo le impediría comprar.
Protección de /api/pedidos, donde sí bloqueamos:
{
"Name": "LimiteApiPedidos",
"Priority": 50,
"Statement": {
"RateBasedStatement": {
"Limit": 2000,
"EvaluationWindowSec": 300,
"AggregateKeyType": "IP",
"ScopeDownStatement": {
"ByteMatchStatement": {
"SearchString": "/api/pedidos",
"FieldToMatch": { "UriPath": {} },
"TextTransformations": [{ "Priority": 0, "Type": "LOWERCASE" }],
"PositionalConstraint": "STARTS_WITH"
}
}
}
},
"Action": { "Block": {} },
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "LimiteApiPedidos"
}
}Cómo se calcula un umbral, con los datos de MercadoFresco: el pico del viernes son 900 pedidos por
hora, es decir 15 por minuto o 75 en la ventana de 5 minutos repartidos entre todos los clientes.
Un solo cliente que hiciera 2.000 peticiones a /api/pedidos en 5 minutos no es un cliente. El umbral
tiene un margen de más de 25 veces sobre el tráfico total del sitio: es conservador a propósito,
porque un umbral demasiado ajustado bloquea antes al cliente raro que al atacante.
La Web ACL de MercadoFresco, completa
Este es el fichero de la Web ACL de CloudFront, en la fase 1 del despliegue: todo en Count.
{
"Name": "waf-mercadofresco-cdn",
"Scope": "CLOUDFRONT",
"DefaultAction": { "Allow": {} },
"Description": "Proteccion de la tienda de MercadoFresco - fase de observacion",
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "wafMercadofrescoCdn"
},
"Rules": [
{
"Name": "BloqueoManual",
"Priority": 0,
"Statement": {
"IPSetReferenceStatement": {
"ARN": "arn:aws:wafv2:us-east-1:111122223333:global/ipset/ipset-mercadofresco-bloqueo/abc123"
}
},
"Action": { "Block": {} },
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "BloqueoManual"
}
},
{
"Name": "ReputacionIp",
"Priority": 10,
"Statement": {
"ManagedRuleGroupStatement": {
"VendorName": "AWS",
"Name": "AWSManagedRulesAmazonIpReputationList"
}
},
"OverrideAction": { "Count": {} },
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "ReputacionIp"
}
},
{
"Name": "ConjuntoComun",
"Priority": 20,
"Statement": {
"ManagedRuleGroupStatement": {
"VendorName": "AWS",
"Name": "AWSManagedRulesCommonRuleSet"
}
},
"OverrideAction": { "Count": {} },
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "ConjuntoComun"
}
},
{
"Name": "EntradasMaliciosas",
"Priority": 25,
"Statement": {
"ManagedRuleGroupStatement": {
"VendorName": "AWS",
"Name": "AWSManagedRulesKnownBadInputsRuleSet"
}
},
"OverrideAction": { "Count": {} },
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "EntradasMaliciosas"
}
},
{
"Name": "InyeccionSql",
"Priority": 30,
"Statement": {
"ManagedRuleGroupStatement": {
"VendorName": "AWS",
"Name": "AWSManagedRulesSQLiRuleSet"
}
},
"OverrideAction": { "Count": {} },
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "InyeccionSql"
}
},
{
"Name": "LimiteLogin",
"Priority": 40,
"Statement": {
"RateBasedStatement": {
"Limit": 100,
"EvaluationWindowSec": 300,
"AggregateKeyType": "IP",
"ScopeDownStatement": {
"ByteMatchStatement": {
"SearchString": "/login",
"FieldToMatch": { "UriPath": {} },
"TextTransformations": [{ "Priority": 0, "Type": "LOWERCASE" }],
"PositionalConstraint": "STARTS_WITH"
}
}
}
},
"Action": { "Count": {} },
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "LimiteLogin"
}
},
{
"Name": "LimiteApiPedidos",
"Priority": 50,
"Statement": {
"RateBasedStatement": {
"Limit": 2000,
"EvaluationWindowSec": 300,
"AggregateKeyType": "IP",
"ScopeDownStatement": {
"ByteMatchStatement": {
"SearchString": "/api/pedidos",
"FieldToMatch": { "UriPath": {} },
"TextTransformations": [{ "Priority": 0, "Type": "LOWERCASE" }],
"PositionalConstraint": "STARTS_WITH"
}
}
}
},
"Action": { "Count": {} },
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "LimiteApiPedidos"
}
}
]
}Observaciones sobre este documento:
- La regla de prioridad 0 sí está en
Block, y es la única. Es el IP set de bloqueo manual: lo rellena Marta a mano durante un incidente, así que no hay riesgo de falso positivo. - Todas las demás están en
Count. Los grupos gestionados con"OverrideAction": {"Count": {}}y las reglas propias con"Action": {"Count": {}}. Son campos distintos para el mismo efecto, y confundirlos es un error habitual. SampledRequestsEnabled: trueen todas. Sin eso no se pueden ver las muestras de peticiones en la consola, que es justamente lo que hace falta en la fase de análisis.- Capacidad total: 1 (IP set) + 25 + 700 + 200 + 200 + 2 + 2 = 1.130 WCU de las 1.500 disponibles.
Queda margen para unas pocas reglas más, pero no para
LinuxRuleSet(200) yAnonymousIpList(50) a la vez sin pedir ampliación.
Creación por CLI:
# 1. El IP set de bloqueo manual (vacío al principio)
aws wafv2 create-ip-set \
--name ipset-mercadofresco-bloqueo \
--scope CLOUDFRONT --region us-east-1 \
--ip-address-version IPV4 --addresses \
--description "Bloqueo manual durante incidentes" \
--tags Key=Proyecto,Value=mercadofresco Key=Componente,Value=waf \
--profile mercadofresco-dev
# 2. La Web ACL
aws wafv2 create-web-acl \
--cli-input-json file:///tmp/waf-mercadofresco-cdn.json \
--region us-east-1 \
--tags Key=Proyecto,Value=mercadofresco Key=Entorno,Value=produccion \
Key=Componente,Value=waf Key=Propietario,Value=marta \
Key=CentroCoste,Value=tecnologia \
--profile mercadofresco-devAñadir una IP al conjunto de bloqueo durante un incidente requiere el LockToken, que actúa como
control de concurrencia optimista:
TOKEN=$(aws wafv2 get-ip-set --name ipset-mercadofresco-bloqueo \
--scope CLOUDFRONT --id <id> --region us-east-1 \
--query 'LockToken' --output text --profile mercadofresco-dev)
aws wafv2 update-ip-set --name ipset-mercadofresco-bloqueo \
--scope CLOUDFRONT --id <id> --region us-east-1 \
--addresses 203.0.113.45/32 198.51.100.0/24 \
--lock-token "$TOKEN" --profile mercadofresco-devCuidado: update-ip-set reemplaza la lista entera, no añade. Hay que leer la lista actual,
añadir la nueva dirección y enviar el conjunto completo. Es un error clásico que borra silenciosamente
bloqueos anteriores.
Asociar la Web ACL a CloudFront y al ALB
CloudFront: se asocia actualizando la configuración de la distribución con el ARN de la Web ACL. El despliegue tarda unos minutos en propagarse a todos los puntos de presencia.
ALB: se asocia directamente y el efecto es inmediato.
aws wafv2 associate-web-acl \
--web-acl-arn arn:aws:wafv2:eu-west-1:111122223333:regional/webacl/waf-mercadofresco-alb/abc123 \
--resource-arn arn:aws:elasticloadbalancing:eu-west-1:111122223333:loadbalancer/app/alb-mercadofresco-tienda/50dc6c495c0c9188 \
--region eu-west-1 --profile mercadofresco-dev
# Comprobar qué recursos protege una Web ACL
aws wafv2 list-resources-for-web-acl \
--web-acl-arn arn:aws:wafv2:eu-west-1:111122223333:regional/webacl/waf-mercadofresco-alb/abc123 \
--region eu-west-1 --profile mercadofresco-devRecuerda el detalle de las reglas por tasa en la Web ACL del ALB: como todo el tráfico llega desde
CloudFront, hay que usar AggregateKeyType: FORWARDED_IP con la cabecera X-Forwarded-For, o el
recuento no distinguirá clientes.
Registros de WAF y su análisis
Sin registros, WAF es una caja negra y la fase 3 del despliegue es imposible. Hay tres destinos:
| Destino | Latencia | Coste | Cuándo |
|---|---|---|---|
| CloudWatch Logs | Segundos | Mayor por GB | Análisis inmediato, alarmas |
| S3 | Minutos | El más barato | Retención larga, análisis con Athena |
| Kinesis Data Firehose | Segundos | Medio | Enviar a un SIEM externo |
MercadoFresco usa CloudWatch Logs durante las dos semanas de observación —porque necesita consultar
enseguida— y luego mantiene el envío a mercadofresco-registros-web para el histórico.
# El grupo debe llamarse obligatoriamente aws-waf-logs-*
aws logs create-log-group --log-group-name aws-waf-logs-mercadofresco \
--region us-east-1 --profile mercadofresco-dev
aws logs put-retention-policy --log-group-name aws-waf-logs-mercadofresco \
--retention-in-days 30 --region us-east-1 --profile mercadofresco-dev
aws wafv2 put-logging-configuration \
--logging-configuration '{
"ResourceArn": "arn:aws:wafv2:us-east-1:111122223333:global/webacl/waf-mercadofresco-cdn/abc123",
"LogDestinationConfigs": ["arn:aws:logs:us-east-1:111122223333:log-group:aws-waf-logs-mercadofresco"],
"RedactedFields": [
{"SingleHeader": {"Name": "authorization"}},
{"SingleHeader": {"Name": "cookie"}},
{"SingleQueryArgument": {"Name": "password"}}
]
}' \
--region us-east-1 --profile mercadofresco-devEl nombre del grupo debe empezar por aws-waf-logs- o la configuración se rechaza sin explicar
por qué. Y RedactedFields no es opcional en un entorno con datos de clientes: sin él, las cookies de
sesión y las cabeceras de autorización acabarían en texto plano en los registros, creando exactamente
el problema que resolvimos en 04-03. Es un punto de cumplimiento del RGPD.
Consultas de la fase 3 con CloudWatch Logs Insights:
-- ¿Qué reglas están contando y cuántas veces?
fields @timestamp, terminatingRuleId, action, httpRequest.uri
| filter action = "COUNT" or terminatingRuleId != "Default_Action"
| stats count(*) as coincidencias by terminatingRuleId
| sort coincidencias desc-- Detalle de las peticiones que una regla concreta habría bloqueado
fields @timestamp, httpRequest.clientIp, httpRequest.uri, httpRequest.country,
httpRequest.headers.0.value
| filter @message like /SizeRestrictions_BODY/
| sort @timestamp desc
| limit 100-- Las 20 IP con más coincidencias
fields httpRequest.clientIp
| filter action = "COUNT"
| stats count(*) as intentos by httpRequest.clientIp
| sort intentos desc
| limit 20Esa primera consulta es la que decide el despliegue: si SQLiRuleSet cuenta 4.000 veces al día y
todas las peticiones son intentos evidentes de inyección, pasa a Block sin dudar. Si CommonRuleSet
cuenta 300 veces y 280 son peticiones legítimas de tu propio formulario de pedido, tienes un falso
positivo que resolver antes.
Métricas y muestras de peticiones bloqueadas
WAF publica en el espacio de nombres AWS/WAFV2:
| Métrica | Qué mide |
|---|---|
AllowedRequests |
Peticiones permitidas |
BlockedRequests |
Peticiones bloqueadas |
CountedRequests |
Coincidencias en modo Count |
CaptchaRequests |
Desafíos CAPTCHA servidos |
PassedRequests |
Que pasaron un desafío |
aws cloudwatch get-metric-statistics \
--namespace AWS/WAFV2 --metric-name BlockedRequests \
--dimensions Name=WebACL,Value=waf-mercadofresco-cdn Name=Rule,Value=ALL Name=Region,Value=CloudFront \
--start-time 2026-08-01T00:00:00Z --end-time 2026-08-02T00:00:00Z \
--period 3600 --statistics Sum \
--region us-east-1 --profile mercadofresco-devY una alarma que avisa cuando algo cambia bruscamente, hacia el mismo tema alertas-mercadofresco de
04-04:
aws cloudwatch put-metric-alarm \
--alarm-name mercadofresco-waf-bloqueos-anomalos \
--alarm-description "Pico de bloqueos en el WAF: posible ataque o falso positivo nuevo" \
--namespace AWS/WAFV2 --metric-name BlockedRequests \
--dimensions Name=WebACL,Value=waf-mercadofresco-cdn Name=Rule,Value=ALL Name=Region,Value=CloudFront \
--statistic Sum --period 300 --evaluation-periods 2 \
--threshold 5000 --comparison-operator GreaterThanThreshold \
--alarm-actions arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
--region us-east-1 --profile mercadofresco-devFíjate en que un pico de bloqueos tiene dos lecturas posibles: te están atacando, o acabas de introducir un falso positivo. Ambas requieren mirar, y por eso la alarma es útil en los dos casos.
Además, la consola ofrece las muestras de peticiones: hasta 100 peticiones de los últimos 3 horas
que coincidieron con cada regla, con sus cabeceras completas. Es la herramienta más rápida para
diagnosticar un falso positivo, y solo funciona si SampledRequestsEnabled está en true.
aws wafv2 get-sampled-requests \
--web-acl-arn arn:aws:wafv2:us-east-1:111122223333:global/webacl/waf-mercadofresco-cdn/abc123 \
--rule-metric-name ConjuntoComun --scope CLOUDFRONT \
--time-window StartTime=2026-08-02T08:00:00Z,EndTime=2026-08-02T10:00:00Z \
--max-items 100 --region us-east-1 --profile mercadofresco-devFalsos positivos: diagnóstico y exclusiones
Un falso positivo es una petición legítima que una regla bloquea. Los cuatro casos que verás:
| Síntoma | Regla habitualmente responsable | Causa |
|---|---|---|
| No se pueden subir fotos grandes de producto | SizeRestrictions_BODY |
El cuerpo supera el límite por defecto |
| Un producto con comillas o apóstrofos en el nombre falla | SQLi_QUERYARGUMENTS |
El apóstrofo parece inyección |
| Una integración interna deja de funcionar | NoUserAgent_HEADER |
El cliente no envía agente de usuario |
| Clientes con VPN corporativa no pueden entrar | AnonymousIpList |
Su salida está catalogada como anónima |
Procedimiento de diagnóstico:
- Identificar la regla exacta. El campo
terminatingRuleIdde los registros, o la columna de la consola, da el nombre concreto dentro del grupo. - Ver la petición completa en las muestras: URI, cabeceras, tamaño.
- Decidir el alcance mínimo de la excepción. Nunca desactives el grupo entero.
- Aplicar y verificar.
Tres formas de resolverlo, de menos a más amplia:
a) Pasar solo esa regla a Count:
b) Excluir esa regla solo en una ruta concreta, combinando con ScopeDownStatement. Es la opción
correcta: la regla sigue protegiendo el resto del sitio.
{
"Name": "ConjuntoComunSalvoSubidas",
"Priority": 20,
"Statement": {
"ManagedRuleGroupStatement": {
"VendorName": "AWS",
"Name": "AWSManagedRulesCommonRuleSet",
"ScopeDownStatement": {
"NotStatement": {
"Statement": {
"ByteMatchStatement": {
"SearchString": "/admin/productos/subir",
"FieldToMatch": { "UriPath": {} },
"TextTransformations": [{ "Priority": 0, "Type": "LOWERCASE" }],
"PositionalConstraint": "STARTS_WITH"
}
}
}
}
}
},
"OverrideAction": { "None": {} },
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "ConjuntoComunSalvoSubidas"
}
}c) Permitir explícitamente antes, con una regla de prioridad menor y acción Allow. Es la más
peligrosa: Allow es terminal, así que esa petición se salta todas las reglas siguientes,
incluidas las de inyección SQL. Úsala solo para tráfico plenamente confiable, como el de la IP de la
oficina.
Y la regla de oro: documenta cada excepción. Una Web ACL con quince exclusiones que nadie recuerda
por qué están es una Web ACL que no protege nada. Cada RuleActionOverride debería tener asociado un
comentario en el repositorio con la fecha, el motivo y una fecha de revisión.
Coste y cálculo para MercadoFresco
| Concepto | Precio |
|---|---|
| Web ACL | 5,00 USD al mes |
| Cada regla o grupo de reglas | 1,00 USD al mes |
| Peticiones | 0,60 USD por millón |
| Grupos gestionados gratuitos | 0 USD (solo cuentan como regla) |
| Bot Control | 10 USD/mes + 1 USD por millón analizado |
| ATP | 10 USD/mes + 1 USD por cada 1.000 intentos de acceso |
| Registros a CloudWatch Logs / S3 | Coste del servicio de destino |
Cálculo para MercadoFresco. Punto clave: la mayor parte del tráfico se sirve desde la caché de CloudFront, y esas peticiones sí pasan por WAF. Contamos 4 millones de peticiones al mes en la distribución y 400.000 que llegan al ALB:
| Concepto | Cantidad | Coste mensual |
|---|---|---|
| Web ACL de CloudFront | 1 | 5,00 USD |
| Reglas en la Web ACL de CloudFront | 7 | 7,00 USD |
| Peticiones (CloudFront) | 4.000.000 | 2,40 USD |
| Web ACL del ALB | 1 | 5,00 USD |
| Reglas en la Web ACL del ALB | 5 | 5,00 USD |
| Peticiones (ALB) | 400.000 | 0,24 USD |
| Registros en CloudWatch Logs | ~3 GB | ~1,50 USD |
| Total | 26,14 USD/mes |
Vale la pena poner esa cifra en contexto:
| Opción | Coste mensual | Qué cubre |
|---|---|---|
| Shield Standard | 0 USD | Volumétricos L3/L4 |
| WAF con esta configuración | 26 USD | Inyección SQL, XSS, bots, inundación L7 |
| Shield Advanced | 3.000 USD | Lo anterior más automatización y créditos |
26 dólares al mes cubren la amenaza más probable para MercadoFresco. Es la decisión de seguridad con mejor relación coste-beneficio del módulo después de KMS, y confirma la evaluación de 04-04: el dinero estaba mucho mejor invertido aquí que en Shield Advanced.
Si el presupuesto apretara, se puede recortar: usar una sola Web ACL en CloudFront y prescindir de la del ALB ahorra 10,24 USD al mes a cambio de perder la defensa en profundidad. Es un compromiso defendible si el ALB está bien cerrado con la cabecera verificada de 04-04.
Limpieza
El orden importa: no se puede eliminar una Web ACL asociada a un recurso.
# 1. Desasociar de cada recurso
aws wafv2 disassociate-web-acl \
--resource-arn arn:aws:elasticloadbalancing:eu-west-1:111122223333:loadbalancer/app/alb-mercadofresco-tienda/50dc6c495c0c9188 \
--region eu-west-1 --profile mercadofresco-dev
# En CloudFront: quitar el WebACLId de la configuración de la distribución
# y esperar a que termine el despliegue
# 2. Quitar la configuración de registros
aws wafv2 delete-logging-configuration \
--resource-arn arn:aws:wafv2:us-east-1:111122223333:global/webacl/waf-mercadofresco-cdn/abc123 \
--region us-east-1 --profile mercadofresco-dev
# 3. Eliminar la Web ACL (necesita el LockToken)
TOKEN=$(aws wafv2 get-web-acl --name waf-mercadofresco-cdn --scope CLOUDFRONT \
--id abc123 --region us-east-1 --query 'LockToken' --output text --profile mercadofresco-dev)
aws wafv2 delete-web-acl --name waf-mercadofresco-cdn --scope CLOUDFRONT \
--id abc123 --lock-token "$TOKEN" --region us-east-1 --profile mercadofresco-dev
# 4. Eliminar el IP set y el grupo de registros
aws wafv2 delete-ip-set --name ipset-mercadofresco-bloqueo --scope CLOUDFRONT \
--id <id> --lock-token <token> --region us-east-1 --profile mercadofresco-dev
aws logs delete-log-group --log-group-name aws-waf-logs-mercadofresco \
--region us-east-1 --profile mercadofresco-devSi dejas la Web ACL creada pero sin asociar, sigue costando 5 USD al mes más 1 USD por regla. Es uno de los cargos fantasma más comunes en las facturas de AWS.
La postura de seguridad de MercadoFresco
Con esta lección se cierra el módulo. Esta es la arquitectura de seguridad completa:
flowchart TD
A["Cliente en internet"] --> B["Route 53 mercadofresco.example<br/>Shield Standard"]
B --> C["CloudFront E2QWERTY123ABC<br/>Shield Standard + cache + TLS de ACM"]
C --> D["AWS WAF waf-mercadofresco-cdn<br/>Reglas gestionadas + por tasa"]
D --> E["ALB alb-mercadofresco-tienda<br/>Cabecera verificada + prefijos CloudFront<br/>waf-mercadofresco-alb"]
E --> F["ASG asg-mercadofresco-tienda<br/>Subredes privadas app-a/-b<br/>rol-mercadofresco-tienda: minimo privilegio"]
F --> G["Secrets Manager<br/>mercadofresco/produccion/rds/mfadmin<br/>rotacion cada 30 dias"]
F --> H["Parameter Store<br/>/mercadofresco/produccion/*"]
G --> I["RDS mercadofresco-pedidos<br/>Multi-AZ, cifrada con KMS<br/>Subredes datos-a/-b sin salida"]
F --> I
C -.->|"OAC oac-mercadofresco-catalogo"| J["S3 mercadofresco-catalogo-fotos<br/>SSE-KMS + clave de bucket"]
K["KMS alias/mercadofresco-datos<br/>Marta administra, los roles usan<br/>rotacion anual"] -.-> I
K -.-> J
K -.-> G
Las cuatro capas, y lo que aporta cada una:
| Capa | Servicios | Qué garantiza |
|---|---|---|
| Identidad | IAM, roles, grupos, MFA | Nadie tiene más permisos de los que necesita; nada lleva claves permanentes |
| Datos | KMS, SSE-KMS, EBS y RDS cifrados | Un disco, un snapshot o un bucket expuesto no revelan nada |
| Secretos | Secrets Manager, Parameter Store | Ninguna credencial vive en un fichero; rotan solas cada 30 días |
| Borde | Shield Standard, WAF, OAC, SG | El tráfico malicioso se descarta lejos y la superficie expuesta es mínima |
Y el coste total del módulo 4:
| Servicio | Coste mensual |
|---|---|
| IAM, Identity Center, Access Analyzer externo | 0,00 USD |
| KMS (1 clave + peticiones) | 1,00 USD |
| Secrets Manager (2 secretos) + Parameter Store | 0,82 USD |
| Shield Standard | 0,00 USD |
| WAF (2 Web ACL, 12 reglas, 4,4 M peticiones) | 26,14 USD |
| Alarmas de CloudWatch | 0,40 USD |
| Total | 28,36 USD/mes |
Menos de 30 dólares al mes por identidades mínimas, datos cifrados, secretos rotados y un borde protegido. Comparado con los 3.000 de Shield Advanced —o con el coste de una brecha notificable bajo el RGPD— es la mejor decisión de inversión de todo el curso.
Errores Comunes y Consejos
Crear la Web ACL de CloudFront en la región equivocada. Debe ser us-east-1 con
--scope CLOUDFRONT. Una Web ACL regional no se puede asociar a una distribución, y el error no lo
dice de forma evidente.
Desplegar directamente en Block. El error más caro de esta disciplina. Dos semanas en Count,
análisis de los registros, exclusiones y solo entonces Block, de una regla en una.
Olvidar las transformaciones de texto. Una regla que busca <script> sin LOWERCASE ni
URL_DECODE se esquiva con %3CSCRIPT%3E. Cuestan 10 WCU cada una y son lo que hace que la regla
sirva para algo.
Confundir OverrideAction con Action. Los grupos de reglas usan OverrideAction; las reglas
propias usan Action. Poner el campo equivocado hace que la API lo rechace o, peor, que el modo
Count que creías haber puesto no esté puesto.
Usar AggregateKeyType: IP en el WAF del ALB detrás de CloudFront. Todas las peticiones parecerán
venir de las IP de CloudFront y la regla por tasa contará todo el tráfico del sitio como una sola
fuente. Ahí hay que usar FORWARDED_IP.
Agotar las WCU sin darse cuenta. CommonRuleSet consume 700 de 1.500. Planifica el presupuesto
antes de añadir grupos, y consulta la capacidad con describe-managed-rule-group.
Reemplazar el IP set en lugar de ampliarlo. update-ip-set sustituye la lista entera. Lee primero,
añade y envía el conjunto completo.
Poner una regla Allow amplia arriba del todo. Allow es terminal: esa petición se salta todas
las reglas de inyección SQL y de tasa que vengan después. Reserva Allow explícito para tráfico
plenamente confiable.
No configurar RedactedFields en los registros. Las cookies de sesión y las cabeceras de
autorización acabarían en texto plano, deshaciendo el trabajo de 04-03 y creando un problema de RGPD.
Dejar una Web ACL creada y sin asociar. Sigue costando 5 USD al mes más 1 USD por regla.
Consejo: versiona la Web ACL en Git. El JSON completo en el repositorio, revisado por otra persona antes de aplicarse, con un comentario por cada exclusión explicando fecha, motivo y revisión prevista. En 09-01 y 09-02 pasará a ser una plantilla de CloudFormation o un constructo de CDK.
Consejo: revisa las exclusiones cada trimestre. Se acumulan, y cada una es un agujero que alguien abrió por una razón que probablemente ya no existe.
Consejo: prefiere Challenge a CAPTCHA. Filtra bots simples sin fricción para el cliente. Guarda
el CAPTCHA para las rutas realmente críticas.
Consejo: ten preparada de antemano la regla de emergencia. Un JSON listo con una regla por tasa agresiva sobre todo el sitio, que Marta pueda aplicar en un minuto durante un incidente, ahorra media hora de escribir bajo presión.
Ejercicios
Ejercicio 1: diseñar la Web ACL de admin.mercadofresco.example
El subdominio de administración que creamos en 03-05 solo debe ser accesible desde la oficina
(192.168.10.0/24 interna, con salida pública 203.0.113.10/32) y desde el teletrabajo de Marta, con
IP dinámica española. Requisitos: nadie más debe llegar; los intentos de acceso desde otros países ni
siquiera deben tocar la aplicación; hay que registrar todos los intentos rechazados; y la solución no
puede impedir que Marta trabaje desde casa cuando le cambie la IP.
Diseña la Web ACL: modelo de acción por defecto, lista de reglas con prioridades y acciones, y justifica el compromiso entre seguridad y usabilidad de la última condición.
Ejercicio 2: analizar la fase de observación y decidir el paso a Block
Tras 14 días con waf-mercadofresco-cdn en Count, los registros dan estos resultados:
| Regla | Coincidencias | Muestra de peticiones |
|---|---|---|
AmazonIpReputationList |
12.400 | Escaneos a /wp-login.php, /.env, /admin.php |
SQLiRuleSet |
3.100 | 3.050 con ' OR '1'='1; 50 son búsquedas de "L'Escala" |
KnownBadInputsRuleSet |
890 | Todas con cadenas de Log4Shell |
CommonRuleSet / SizeRestrictions_BODY |
420 | 415 son subidas de fotos desde /admin/productos/subir |
CommonRuleSet / NoUserAgent_HEADER |
310 | 295 del monitor interno de Marta; 15 de escáneres |
CommonRuleSet / CrossSiteScripting_BODY |
45 | Todas maliciosas |
LimiteLogin |
8 | 6 de una IP con 400 intentos; 2 de la oficina en hora punta |
LimiteApiPedidos |
0 | — |
Para cada regla, decide: pasar a Block, mantener en Count, o aplicar una exclusión concreta. Para
las que requieran exclusión, escribe el JSON. Justifica cada decisión y di en qué orden aplicarías los
cambios.
Ejercicio 3: escribir la respuesta a un incidente en caliente
Un viernes a las 18:40, en pleno pico, MercadoFresco recibe un ataque de capa 7: 30.000 peticiones por
minuto a /buscar?q=<aleatorio> desde 5.000 IP de 40 países, con agentes de usuario variados y
realistas. La tasa de aciertos de caché se ha hundido al 8 % y DatabaseConnections está en 185 de
200. La Web ACL está desplegada y en Block para las reglas gestionadas.
Escribe la secuencia exacta de acciones de Marta durante los primeros 15 minutos, incluyendo el JSON de las reglas que aplicaría, el riesgo de cada acción sobre los clientes legítimos que están comprando en ese momento, y qué comprobaría después de cada paso. Ten en cuenta que es viernes a las 18:40: es el peor momento posible para bloquear tráfico real.
Soluciones
Solución 1
Modelo: acción por defecto Block (lista blanca). Es un panel de administración, no un sitio
público: lo correcto es negar todo y permitir lo explícito.
| Prioridad | Regla | Declaración | Acción |
|---|---|---|---|
| 0 | OficinaPermitida |
IPSetReferenceStatement → ipset-mercadofresco-oficina (203.0.113.10/32) |
Allow |
| 10 | SoloEspana |
NotStatement(GeoMatchStatement ES) |
Block |
| 20 | MartaConDesafio |
ByteMatchStatement sobre /admin |
Challenge |
| 30 | ProteccionesBase |
AWSManagedRulesCommonRuleSet + KnownBadInputs |
Block |
| — | Por defecto | — | Block |
Cómo funciona el recorrido: el tráfico de la oficina coincide en prioridad 0 y se permite de
inmediato, saltándose todo lo demás. El de fuera de España se bloquea en la 10. Lo que queda —tráfico
español que no es de la oficina, es decir, potencialmente Marta desde casa— recibe un Challenge
silencioso en la 20, que un navegador real resuelve solo y un script no. Todo lo demás cae en el
Block por defecto.
El compromiso de la última condición. La opción máximamente segura sería permitir solo las IP
del IP set, pero eso obliga a Marta a llamar a alguien cada vez que su operador le cambia la IP, lo
cual acaba siempre en que alguien añade 0.0.0.0/0 «temporalmente» un domingo. La solución propuesta
acepta un riesgo controlado: cualquiera con IP española que llegue a /admin supera el Challenge si
usa un navegador. Pero eso no le da acceso: sigue habiendo autenticación con MFA detrás, y WAF es
solo la primera barrera. Lo que se consigue es eliminar el 99,9 % del ruido automatizado sin bloquear
a la persona que administra el sistema.
La alternativa mejor, si se quiere subir el listón sin perder usabilidad, es poner el panel detrás de
una VPN o de un cliente de Verified Access con IP de salida fija, y volver al modelo de lista blanca
estricta. Registro: la configuración de registros de la Web ACL captura todos los bloqueos, con
RedactedFields sobre authorization y cookie.
Solución 2
| Regla | Decisión | Justificación |
|---|---|---|
AmazonIpReputationList |
Block |
12.400 coincidencias, cero falsos positivos: escaneos puros contra rutas que MercadoFresco ni siquiera tiene |
SQLiRuleSet |
Block con exclusión |
98 % son ataques reales, pero 50 son búsquedas legítimas de topónimos con apóstrofo |
KnownBadInputsRuleSet |
Block |
890 intentos de Log4Shell, ningún falso positivo |
SizeRestrictions_BODY |
Exclusión de ruta | 415 de 420 son subidas legítimas de fotos de producto |
NoUserAgent_HEADER |
Exclusión o arreglar el monitor | 295 de 310 son el monitor interno |
CrossSiteScripting_BODY |
Block |
45 coincidencias, todas maliciosas |
LimiteLogin |
Block... no: Captcha |
6 de 8 son un ataque claro, pero 2 son la oficina |
LimiteApiPedidos |
Block |
Cero coincidencias en 14 días: el umbral de 2.000 es holgado y no hay riesgo |
Exclusión para SizeRestrictions_BODY (opción b, la correcta: la regla sigue activa en el resto
del sitio):
{
"Name": "ConjuntoComun",
"Priority": 20,
"Statement": {
"ManagedRuleGroupStatement": {
"VendorName": "AWS",
"Name": "AWSManagedRulesCommonRuleSet",
"ScopeDownStatement": {
"NotStatement": {
"Statement": {
"ByteMatchStatement": {
"SearchString": "/admin/productos/subir",
"FieldToMatch": { "UriPath": {} },
"TextTransformations": [{ "Priority": 0, "Type": "LOWERCASE" }],
"PositionalConstraint": "STARTS_WITH"
}
}
}
},
"RuleActionOverrides": [
{ "Name": "NoUserAgent_HEADER", "ActionToUse": { "Count": {} } }
]
}
},
"OverrideAction": { "None": {} },
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "ConjuntoComun"
}
}Para SQLiRuleSet, las 50 búsquedas de "L'Escala" no justifican desactivar la protección contra
inyección SQL en una tienda con PostgreSQL detrás. Hay dos salidas y la buena no está en WAF:
- Correcta: arreglar el frontal para que codifique correctamente el apóstrofo antes de enviarlo, o usar POST con JSON en lugar de la cadena de consulta. El falso positivo desaparece solo.
- Aceptable como parche temporal: aplicar
RuleActionOverridessolo sobreSQLi_QUERYARGUMENTSy únicamente en la ruta/buscar, dejando activo el resto del grupo. Con fecha de revisión anotada.
Para NoUserAgent_HEADER, la solución de fondo no es una excepción en WAF sino arreglar el
monitor para que envíe un agente de usuario identificativo (MercadoFresco-Monitor/1.0). Mientras
tanto, la excepción de arriba lo mantiene en Count.
Para LimiteLogin, Captcha en lugar de Block: las 2 coincidencias de la oficina son una NAT
compartida en hora punta, y un bloqueo impediría trabajar a todo el equipo. El CAPTCHA deja pasar a
las personas y detiene el ataque de 400 intentos.
Orden de aplicación, siguiendo la tabla de riesgo:
AmazonIpReputationList→Block. Vigilar 24 h.KnownBadInputsRuleSet→Block. Vigilar 24 h.LimiteApiPedidos→BlockyLimiteLogin→Captcha. Vigilar 24 h.- Arreglar el monitor y el frontal de búsqueda (cambios de aplicación, no de WAF).
SQLiRuleSet→Block, con la exclusión de/buscarsi el frontal aún no está arreglado.CommonRuleSet→Blockcon la exclusión de ruta para las subidas. El último y el más vigilado: es el grupo más amplio.
Y ninguno de estos cambios se aplica un viernes.
Solución 3
Contexto que condiciona todo: viernes 18:40, pico de 900 pedidos/hora, clientes reales comprando. Cada minuto de bloqueo indiscriminado son pedidos perdidos. La prioridad es mantener el servicio, no ganar el combate.
Minutos 0-2: confirmar y caracterizar. No se toca nada todavía.
Se miran las tres métricas que deciden: PedidosPorHora del espacio MercadoFresco/Tienda (si sigue
en ~900, hay clientes reales comprando ahora mismo), CacheHitRate (8 %, confirmado) y
DatabaseConnections (185/200, crítico). Con agentes de usuario realistas y 5.000 IP repartidas, el
patrón identificable no es el origen sino la ruta.
Minutos 2-4: aliviar la base de datos, que es lo que va a caer. Es la acción de menor riesgo y mayor efecto, y no bloquea a nadie:
- Aumentar el TTL mínimo de las respuestas de
/buscaren CloudFront a 60 segundos. - Reducir la clave de caché de
/buscarpara que solo incluyaqnormalizado, ignorando los demás parámetros aleatorios. Esto convierte parte del ataque en aciertos de caché.
Riesgo para clientes legítimos: resultados de búsqueda hasta un minuto desactualizados. Irrelevante.
Comprobar después: CacheHitRate y DatabaseConnections en los 3 minutos siguientes.
Minutos 4-7: regla por tasa sobre /buscar, primero en Count.
{
"Name": "EmergenciaBuscar",
"Priority": 45,
"Statement": {
"RateBasedStatement": {
"Limit": 300,
"EvaluationWindowSec": 60,
"AggregateKeyType": "IP",
"ScopeDownStatement": {
"ByteMatchStatement": {
"SearchString": "/buscar",
"FieldToMatch": { "UriPath": {} },
"TextTransformations": [{ "Priority": 0, "Type": "LOWERCASE" }],
"PositionalConstraint": "STARTS_WITH"
}
}
}
},
"Action": { "Count": {} },
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "EmergenciaBuscar"
}
}Riesgo: ninguno, Count no bloquea. Comprobar: CountedRequests de la regla y las muestras. Si
las peticiones contadas son las del ataque y no clientes normales, se pasa al siguiente paso. Sí,
se cuentan dos minutos incluso en una emergencia: es exactamente el momento en que un error cuesta
más caro.
Minutos 7-9: pasar a Challenge, no a Block.
Se cambia "Action": { "Count": {} } por "Action": { "Challenge": {} }. Un navegador real resuelve
el desafío de JavaScript de forma invisible y sigue comprando; un bot que no ejecuta JavaScript no
pasa.
Riesgo: mínimo. Solo afectaría a clientes con JavaScript desactivado, prácticamente inexistentes en
una tienda que ya lo requiere. Comprobar: PassedRequests frente a BlockedRequests,
DatabaseConnections y, sobre todo, PedidosPorHora: si sigue en 900, no estamos dañando el
negocio.
Minutos 9-12: si el ataque persiste, endurecer solo a los peores.
Si DatabaseConnections sigue por encima de 180, se añade una segunda regla por tasa con umbral muy
alto (por ejemplo 1.000 por minuto por IP sobre /buscar) en Block. Ese umbral es imposible de
alcanzar para una persona, así que el riesgo de falso positivo es casi nulo.
Comprobar: que PedidosPorHora no cae.
Minutos 12-15: capacidad y comunicación.
- Derivar las lecturas de búsqueda a la réplica
mercadofresco-pedidos-lecturasi la aplicación lo admite por configuración, para descargar la instancia principal. - Subir temporalmente el máximo del ASG.
- Avisar a Luis y a Sara del estado, y anotar la hora de cada acción para el post-mortem.
Lo que Marta NO hace, y es tan importante como lo que hace:
- No bloquea por país. 40 países, y algunos tendrán clientes españoles de viaje.
- No bloquea las 5.000 IP. Volverán con otras y bloquearía clientes que comparten NAT.
- No pasa
CommonRuleSeta modo agresivo ni toca reglas que no ha probado: un falso positivo nuevo a las 18:40 de un viernes es peor que el ataque. - No apaga la búsqueda entera, que es una funcionalidad central de una tienda de alimentación.
- No desactiva CloudFront para «ver si es cosa suya»: eliminaría la única capa que la protege.
- No aplica ningún cambio permanente en caliente. Las reglas de emergencia se marcan como temporales y se revisan el lunes con datos, no el viernes con adrenalina.
Conclusión
MercadoFresco tiene por fin la capa que faltaba. Sabes qué ve AWS WAF que ni un grupo de seguridad
ni Shield pueden ver —el método, la ruta, la cadena de consulta, las cabeceras, las cookies y el cuerpo
de cada petición HTTP— y por qué las tres herramientas son complementarias y ninguna sustituye a otra.
Conoces la anatomía de una Web ACL: reglas con prioridad, grupos gestionados, IP sets, el
presupuesto de WCU que CommonRuleSet consume casi a la mitad, y las cinco acciones —con Allow
y Block terminales, Count nunca terminal, y Challenge como la opción que filtra bots sin cobrar
fricción al cliente.
Sabes que la Web ACL de CloudFront se crea en us-east-1 con --scope CLOUDFRONT, la misma
trampa que ACM y las métricas de la CDN. Dominas las declaraciones de coincidencia y, sobre todo,
las transformaciones de texto: sin LOWERCASE ni URL_DECODE, una regla contra <script> se
esquiva escribiendo %3CSCRIPT%3E, y por eso cada transformación cuesta 10 WCU bien gastadas. Conoces
los grupos gestionados por AWS —CommonRuleSet, KnownBadInputs, SQLi, Linux,
AmazonIpReputationList, AnonymousIpList, y los de pago BotControl y ATP— y que los cuatro
primeros son gratis y cubren la inmensa mayoría de lo que verás.
Y por encima de todo dominas el método de despliegue, que es lo que separa una Web ACL útil de un
apagón autoinfligido: todo en Count, dos semanas incluyendo dos viernes, analizar los registros,
excluir con el alcance mínimo, y solo entonces pasar a Block de una regla en una, empezando por la
que menos falsos positivos produce. Has montado las reglas por tasa que protegen /login con
Captcha y /api/pedidos con Block, sabiendo calcular el umbral a partir del tráfico real y usar
FORWARDED_IP cuando el WAF está en el ALB detrás de CloudFront. Tienes los registros en
aws-waf-logs-mercadofresco con RedactedFields sobre cookies y cabeceras de autorización, las
consultas de Insights que deciden el paso a Block, las métricas de AWS/WAFV2 con su alarma hacia
alertas-mercadofresco, y el procedimiento para diagnosticar y acotar un falso positivo sin desactivar
un grupo entero. Todo por 26,14 USD al mes, frente a los 3.000 de Shield Advanced.
Con esto se cierra el módulo 4. La postura de seguridad de MercadoFresco descansa sobre cuatro
capas: identidades mínimas con roles sin claves permanentes, grupos humanos y MFA obligatorio;
datos cifrados con alias/mercadofresco-datos, separación de funciones y rotación anual;
secretos custodiados en mercadofresco/produccion/rds/mfadmin con rotación automática cada 30
días; y un borde protegido por Shield Standard, dos Web ACL y una superficie expuesta reducida al
mínimo. Todo ello por 28,36 USD mensuales, y con una decisión razonada y documentada de no
contratar Shield Advanced.
Y sin embargo, hay algo profundamente incompleto en todo esto, y es lo que da nombre al módulo siguiente. Hemos construido alarmas que envían avisos a un tema de SNS, pero nadie ha comprobado nunca que ese aviso llegue de verdad a un teléfono a las cuatro de la madrugada. Los registros de WAF, de la VPC, del ALB y de las funciones Lambda se están acumulando en cinco sitios distintos sin que nadie los correlacione. Cuando un cliente escriba diciendo que su pedido tarda ocho segundos en confirmarse, Marta no tendrá forma de saber si el problema está en la tienda, en la Lambda de estado de pedido o en la base de datos. Nadie sabe quién descifró la última copia de la base de datos, ni cuándo, aunque KMS lo haya registrado escrupulosamente. Y no existe ningún mecanismo que avise si mañana alguien desactiva el cifrado de un bucket o abre un grupo de seguridad al mundo.
Dicho de otro modo: está todo montado, y nadie está mirando. En el módulo 5, «Monitorización y
gestión», empezando por la lección 05-01 «Amazon CloudWatch», construiremos esa mirada: métricas,
paneles, registros centralizados y alarmas que funcionan de verdad; después el rastreo de una petición
de extremo a extremo con X-Ray, la auditoría de cada llamada a la API con CloudTrail —donde
por fin veremos quién usó kms:Decrypt y quién leyó qué secreto—, la vigilancia continua del
cumplimiento con AWS Config, que avisa en cuanto una configuración se desvía de lo que acabamos de
construir, y las recomendaciones automáticas de Trusted Advisor.
Curso de AWS
Módulo 1: Introducción a AWS
- ¿Qué es AWS?
- Configuración de tu cuenta de AWS
- Infraestructura global de AWS
- Consola de administración de AWS
- AWS CLI y SDKs
Módulo 2: Servicios principales de AWS
Módulo 3: Redes y entrega de contenido
- Amazon VPC
- Grupos de seguridad y listas de control de acceso
- Elastic Load Balancing
- Amazon CloudFront
- Route 53
Módulo 4: Seguridad e identidad
- AWS Identity and Access Management (IAM)
- AWS Key Management Service (KMS)
- Secrets Manager y Parameter Store
- AWS Shield
- AWS WAF
Módulo 5: Monitorización y gestión
- Amazon CloudWatch
- AWS X-Ray y trazabilidad distribuida
- AWS CloudTrail
- AWS Config
- AWS Trusted Advisor
Módulo 6: Bases de datos
- Cómo elegir la base de datos adecuada
- Amazon DynamoDB
- Amazon Aurora
- Amazon Redshift
- Amazon ElastiCache
Módulo 7: Integración de aplicaciones
- Amazon SQS
- Amazon SNS
- Amazon EventBridge
- AWS Step Functions
- Patrones de integración: idempotencia, reintentos y colas de mensajes fallidos
