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

  1. Qué ve WAF que un grupo de seguridad no puede ver
  2. Dónde se asocia una Web ACL
  3. Componentes: Web ACL, reglas y grupos de reglas
  4. Unidades de capacidad (WCU)
  5. Acciones y prioridad de evaluación
  6. El recorrido de una petición
  7. Declaraciones de coincidencia
  8. Transformaciones de texto
  9. Grupos de reglas gestionadas por AWS
  10. El método de despliegue: Count primero, Block después
  11. Reglas basadas en tasa
  12. La Web ACL de MercadoFresco, completa
  13. Asociar la Web ACL a CloudFront y al ALB
  14. Registros de WAF y su análisis
  15. Métricas y muestras de peticiones bloqueadas
  16. Falsos positivos: diagnóstico y exclusiones
  17. Coste y cálculo para MercadoFresco
  18. Limpieza
  19. La postura de seguridad de MercadoFresco
  20. 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
Bloquea por país No No
Limita peticiones por IP No No
Distingue navegador de bot No No
Coste Gratis Gratis 5 USD/mes + reglas + peticiones

Un ejemplo concreto que resume la diferencia. Esta petición:

GET /buscar?q=tomate'%20OR%20'1'='1 HTTP/1.1
Host: mercadofresco.example
User-Agent: sqlmap/1.7

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-dev

Y 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-dev

Acciones 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:

  1. Allow y Block son 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.
  2. Count nunca 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
&lt;script&gt; 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 &lt; 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 : hay PostgreSQL detrás
AWSManagedRulesLinuxRuleSet 200 Gratis LFI, inclusión de /etc/passwd, ejecución de comandos : 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 , 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. Count registra 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 usa Count.
  • 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: usa X-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: true en 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) y AnonymousIpList (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-dev

Añ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-dev

Cuidado: 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-dev

Recuerda 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-dev

El 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 20

Esa 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-dev

Y 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-dev

Fí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-dev

Falsos 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:

  1. Identificar la regla exacta. El campo terminatingRuleId de los registros, o la columna de la consola, da el nombre concreto dentro del grupo.
  2. Ver la petición completa en las muestras: URI, cabeceras, tamaño.
  3. Decidir el alcance mínimo de la excepción. Nunca desactives el grupo entero.
  4. Aplicar y verificar.

Tres formas de resolverlo, de menos a más amplia:

a) Pasar solo esa regla a Count:

"RuleActionOverrides": [
  { "Name": "SizeRestrictions_BODY", "ActionToUse": { "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-dev

Si 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 IPSetReferenceStatementipset-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 RuleActionOverrides solo sobre SQLi_QUERYARGUMENTS y ú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:

  1. AmazonIpReputationListBlock. Vigilar 24 h.
  2. KnownBadInputsRuleSetBlock. Vigilar 24 h.
  3. LimiteApiPedidosBlock y LimiteLoginCaptcha. Vigilar 24 h.
  4. Arreglar el monitor y el frontal de búsqueda (cambios de aplicación, no de WAF).
  5. SQLiRuleSetBlock, con la exclusión de /buscar si el frontal aún no está arreglado.
  6. CommonRuleSetBlock con 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.

curl -sS -o /dev/null -w "%{http_code} %{time_total}s\n" https://mercadofresco.example/salud

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 /buscar en CloudFront a 60 segundos.
  • Reducir la clave de caché de /buscar para que solo incluya q normalizado, 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-lectura si 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 CommonRuleSet a 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 AWSCommonRuleSet, 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

Módulo 2: Servicios principales de AWS

Módulo 3: Redes y entrega de contenido

Módulo 4: Seguridad e identidad

Módulo 5: Monitorización y gestión

Módulo 6: Bases de datos

Módulo 7: Integración de aplicaciones

Módulo 8: Herramientas para desarrolladores

Módulo 9: Infraestructura como código y gobierno de cuentas

Módulo 10: Contenedores en AWS

Módulo 11: Mejores prácticas y gestión de costos

© Copyright 2026. Todos los derechos reservados