Lab práctico · Semana 6: Redes en AWS (II): DNS, borde de red y protección

AWS WAF delante de CloudFront y failover con Route 53 (sin comprar dominio)

⏱ 90-120 minDificultad: avanzadaTask statements: 1.22.23.4

Qué vas a construir

Sobre la distribución de CloudFront del lab 11:

  1. AWS WAF: una web ACL en us-east-1 con una regla rate-based (límite por IP), el Core rule set y el grupo de inyección SQL de las AWS Managed Rules. Verás cómo bloquea XSS, SQLi y una ráfaga de peticiones.
  2. Route 53 sin comprar dominio: una zona pública para un dominio inventado (que borrarás en menos de 12 horas, así que no se cobra), un health check, registros failover y weighted, y la comprobación de las respuestas con test-dns-answer, que consulta directamente los servidores de Route 53.
  3. Opcional (de pago): tu propio dominio con certificado de ACM en us-east-1 y un registro alias hacia CloudFront.
flowchart LR
  u["curl desde CloudShell"]
  waf["AWS WAF (us-east-1)<br/>rate-based + Core rule set + SQLi"]
  cf["CloudFront (lab 11)"]
  s3[("S3 privado")]
  r53["Route 53<br/>zona lab12-xxxx.com"]
  hc["Health check HTTP<br/>contra CloudFront"]
  u --> waf --> cf --> s3
  hc --> cf
  r53 -- "failover PRIMARY si hc sano" --> p["192.0.2.10"]
  r53 -. "SECONDARY si hc no sano" .-> s["198.51.100.20"]

Antes de empezar

  • Tener desplegada la pila lab11-cdn del lab 11 y el fichero lab11.yaml en CloudShell (si la borraste, vuelve a hacer los pasos 1 a 3 del lab 11).
  • Usuario de IAM Identity Center o IAM con MFA (nunca root), CloudShell en eu-south-2. Las llamadas a WAF para CloudFront irán a us-east-1 con --region us-east-1.
source ~/lab11.env
echo $CF $DIST
curl -s -o /dev/null -w "%{http_code}\n" https://$CF/

Debe responder 200.

Parte A: AWS WAF

Paso 1: definir las reglas

Crea reglas.json. La regla 0 es rate-based (máximo 10 peticiones por IP en 60 s, el mínimo que admite WAF, para poder provocarla a mano); la 1 y la 2 son managed rule groups de AWS.

cat > reglas.json <<'EOF'
[
  {
    "Name": "limite-por-ip",
    "Priority": 0,
    "Statement": {
      "RateBasedStatement": {
        "Limit": 10,
        "EvaluationWindowSec": 60,
        "AggregateKeyType": "IP"
      }
    },
    "Action": { "Block": {} },
    "VisibilityConfig": {
      "SampledRequestsEnabled": true,
      "CloudWatchMetricsEnabled": true,
      "MetricName": "limite-por-ip"
    }
  },
  {
    "Name": "aws-core-rule-set",
    "Priority": 1,
    "Statement": {
      "ManagedRuleGroupStatement": { "VendorName": "AWS", "Name": "AWSManagedRulesCommonRuleSet" }
    },
    "OverrideAction": { "None": {} },
    "VisibilityConfig": {
      "SampledRequestsEnabled": true,
      "CloudWatchMetricsEnabled": true,
      "MetricName": "aws-core-rule-set"
    }
  },
  {
    "Name": "aws-sqli",
    "Priority": 2,
    "Statement": {
      "ManagedRuleGroupStatement": { "VendorName": "AWS", "Name": "AWSManagedRulesSQLiRuleSet" }
    },
    "OverrideAction": { "None": {} },
    "VisibilityConfig": {
      "SampledRequestsEnabled": true,
      "CloudWatchMetricsEnabled": true,
      "MetricName": "aws-sqli"
    }
  }
]
EOF

Fíjate en la diferencia: las reglas propias llevan Action (Block, Allow, Count, CAPTCHA, Challenge); los grupos gestionados llevan OverrideAction (None = respetar las acciones del grupo; Count = solo contar, útil para probar un grupo nuevo sin romper nada).

Paso 2: crear la web ACL en us-east-1

Para CloudFront, la web ACL tiene que estar en us-east-1 con ámbito CLOUDFRONT:

read ACL_ID ACL_ARN <<< $(aws wafv2 create-web-acl --region us-east-1 --name lab12-acl --scope CLOUDFRONT \
  --default-action Allow={} \
  --visibility-config SampledRequestsEnabled=true,CloudWatchMetricsEnabled=true,MetricName=lab12-acl \
  --rules file://reglas.json \
  --query '[Summary.Id,Summary.ARN]' --output text)
echo "export ACL_ID=$ACL_ID ACL_ARN=$ACL_ARN" >> ~/lab11.env
echo $ACL_ARN

Paso 3: asociarla a la distribución

Con CloudFront no se usa associate-web-acl: la web ACL se indica en la configuración de la distribución (WebACLId). Como la pila del lab 11 tiene el parámetro preparado, basta con actualizarla:

aws cloudformation deploy --stack-name lab11-cdn --template-file lab11.yaml \
  --parameter-overrides WebAclArn=$ACL_ARN

Espera a que termine (la distribución se vuelve a desplegar, unos minutos).

Paso 4: probar XSS e inyección SQL

codigo () { curl -s -o /dev/null -w "%{http_code}\n" "$@"; }
codigo https://$CF/
codigo --get --data-urlencode "q=<script>alert(1)</script>" https://$CF/
codigo --get --data-urlencode "id=1' OR '1'='1" https://$CF/

La primera devuelve 200; la segunda y la tercera, 403: las han bloqueado las reglas CrossSiteScripting_QUERYARGUMENTS del Core rule set y las del grupo de SQLi. No has escrito ni una expresión regular.

Paso 5: provocar la regla rate-based

Lanza unas 60 peticiones seguidas (una por segundo):

for i in $(seq 1 60); do curl -s -o /dev/null -w "%{http_code} " https://$CF/; sleep 1; done; echo

Al principio verás 200; en cuanto WAF detecta que tu IP supera 10 peticiones en 60 s (lo comprueba cada pocos segundos), empiezan los 403. Deja de hacer peticiones un par de minutos y vuelve a probar: tu IP se desbloquea sola cuando la tasa baja.

Mira qué IP está limitando la regla y algunas peticiones de muestra:

aws wafv2 get-rate-based-statement-managed-keys --region us-east-1 --scope CLOUDFRONT \
  --web-acl-name lab12-acl --web-acl-id $ACL_ID --rule-name limite-por-ip
aws wafv2 get-sampled-requests --region us-east-1 --scope CLOUDFRONT --web-acl-arn $ACL_ARN \
  --rule-metric-name aws-core-rule-set --max-items 10 \
  --time-window StartTime=$(date -u -d '-30 minutes' +%Y-%m-%dT%H:%M:%SZ),EndTime=$(date -u +%Y-%m-%dT%H:%M:%SZ) \
  --query 'SampledRequests[].[Action,Request.URI,Request.ClientIP]' --output table

En la consola de AWS WAF & Shield (región Global (CloudFront)) puedes ver las métricas de cada regla y el panel de tráfico.

Parte B: Route 53 sin comprar dominio

Route 53 te deja crear una zona pública para cualquier nombre, aunque no lo hayas registrado. Nadie en internet llegará a ella (el dominio no está delegado a tus servidores de nombres), pero puedes consultar sus respuestas con aws route53 test-dns-answer, que pregunta directamente a los servidores de Route 53 de la zona. Es perfecto para aprender las políticas de enrutamiento gratis.

Paso 6: crear la zona y el health check

ZONA=lab12-$RANDOM$RANDOM.com
ZONE_ID=$(aws route53 create-hosted-zone --name $ZONA --caller-reference lab12-$(date +%s) \
  --query HostedZone.Id --output text | cut -d/ -f3)
echo "export ZONA=$ZONA ZONE_ID=$ZONE_ID" >> ~/lab11.env
date
aws route53 list-resource-record-sets --hosted-zone-id $ZONE_ID --query 'ResourceRecordSets[].[Name,Type]' --output table

Route 53 ha creado los registros NS y SOA. Apunta la hora: borra la zona antes de 12 horas para que no se cobre.

Crea un health check HTTP en el puerto 80 contra la distribución (CloudFront responde 301 hacia HTTPS, y Route 53 considera sanos los códigos 2xx y 3xx):

HC_ID=$(aws route53 create-health-check --caller-reference lab12-hc-$(date +%s) \
  --health-check-config Type=HTTP,FullyQualifiedDomainName=$CF,Port=80,ResourcePath=/,RequestInterval=30,FailureThreshold=3 \
  --query HealthCheck.Id --output text)
echo "export HC_ID=$HC_ID" >> ~/lab11.env

Tras uno o dos minutos, mira qué dicen los health checkers repartidos por el mundo:

aws route53 get-health-check-status --health-check-id $HC_ID \
  --query 'HealthCheckObservations[].[Region,StatusReport.Status]' --output table

Paso 7: registros failover

Usamos direcciones de documentación (192.0.2.0/24 y 198.51.100.0/24, reservadas para ejemplos) como «región primaria» y «región secundaria»:

cat > failover.json <<EOF
{
  "Changes": [
    {
      "Action": "CREATE",
      "ResourceRecordSet": {
        "Name": "app.$ZONA", "Type": "A", "TTL": 60,
        "SetIdentifier": "primario", "Failover": "PRIMARY",
        "HealthCheckId": "$HC_ID",
        "ResourceRecords": [{ "Value": "192.0.2.10" }]
      }
    },
    {
      "Action": "CREATE",
      "ResourceRecordSet": {
        "Name": "app.$ZONA", "Type": "A", "TTL": 60,
        "SetIdentifier": "secundario", "Failover": "SECONDARY",
        "ResourceRecords": [{ "Value": "198.51.100.20" }]
      }
    }
  ]
}
EOF
aws route53 change-resource-record-sets --hosted-zone-id $ZONE_ID --change-batch file://failover.json
aws route53 test-dns-answer --hosted-zone-id $ZONE_ID --record-name app.$ZONA --record-type A \
  --query RecordData --output text

Responde 192.0.2.10 (el primario, porque su health check está sano).

Paso 8: simular la caída del primario

En vez de romper CloudFront, invierte el health check (una opción de Route 53 que cambia sano por no sano; no tiene coste adicional):

aws route53 update-health-check --health-check-id $HC_ID --inverted
sleep 120
aws route53 get-health-check-status --health-check-id $HC_ID --query 'HealthCheckObservations[0].StatusReport.Status' --output text
aws route53 test-dns-answer --hosted-zone-id $ZONE_ID --record-name app.$ZONA --record-type A \
  --query RecordData --output text

Cuando el estado pasa a no sano (necesita 3 fallos seguidos cada 30 s), la respuesta cambia a 198.51.100.20: failover activo-pasivo. Restaura el health check:

aws route53 update-health-check --health-check-id $HC_ID --no-inverted

Paso 9: registros weighted

Dos registros para web con pesos 80 y 20:

cat > weighted.json <<EOF
{
  "Changes": [
    { "Action": "CREATE", "ResourceRecordSet": { "Name": "web.$ZONA", "Type": "A", "TTL": 60,
      "SetIdentifier": "estable", "Weight": 80, "ResourceRecords": [{ "Value": "192.0.2.80" }] } },
    { "Action": "CREATE", "ResourceRecordSet": { "Name": "web.$ZONA", "Type": "A", "TTL": 60,
      "SetIdentifier": "canary", "Weight": 20, "ResourceRecords": [{ "Value": "192.0.2.20" }] } }
  ]
}
EOF
aws route53 change-resource-record-sets --hosted-zone-id $ZONE_ID --change-batch file://weighted.json
for i in $(seq 1 20); do
  aws route53 test-dns-answer --hosted-zone-id $ZONE_ID --record-name web.$ZONA --record-type A \
    --query 'RecordData[0]' --output text
done | sort | uniq -c

Verás aproximadamente 16 respuestas 192.0.2.80 y 4 192.0.2.20 (es aleatorio: con 20 muestras varía). Así se hace un despliegue canary por DNS. Poner el peso de un registro a 0 deja de enviarle tráfico sin borrarlo.

Paso 10 (opcional, gratis): zona privada

Si todavía tienes la pila del lab 10, crea una zona privada asociada a la VPC A (aws route53 create-hosted-zone --name interno.lab --vpc VPCRegion=eu-south-2,VPCId=$VpcA --caller-reference priv-$(date +%s)), añade un registro A y compruébalo desde la instancia A con getent hosts nombre.interno.lab. Desde CloudShell no resolverá: la zona solo existe dentro de las VPC asociadas. Bórrala en menos de 12 horas.

Parte C (opcional, de pago): tu propio dominio con HTTPS

Hazlo solo si ya tienes un dominio o quieres comprar uno (el coste anual depende del TLD; consulta la lista de precios de Route 53 antes de registrarlo). Pasos:

  1. Dominio: regístralo en Route 53 (Route 53 → Registered domains) o, si lo tienes en otro registrador, crea una zona pública en Route 53 y cambia allí los NS del dominio por los de la zona.
  2. Certificado en us-east-1 con validación DNS:
aws acm request-certificate --region us-east-1 --domain-name www.tudominio.com \
  --validation-method DNS --query CertificateArn --output text

En la consola de ACM (región N. Virginia), pulsa Create records in Route 53 para crear el CNAME de validación. Con validación DNS, ACM renovará el certificado solo. 3. CloudFront: en la distribución, Edit → Alternate domain name www.tudominio.com y Custom SSL certificate el de ACM (SNI). (Si lo haces en la consola, la pila de CloudFormation tendrá drift; para producción, añade Aliases y ViewerCertificate a la plantilla). 4. Registro alias en tu zona (el HostedZoneId de cualquier distribución de CloudFront es siempre Z2FDTNDATAQYW2):

{
  "Changes": [
    {
      "Action": "CREATE",
      "ResourceRecordSet": {
        "Name": "www.tudominio.com",
        "Type": "A",
        "AliasTarget": {
          "HostedZoneId": "Z2FDTNDATAQYW2",
          "DNSName": "d111111abcdef8.cloudfront.net",
          "EvaluateTargetHealth": false
        }
      }
    }
  ]
}

(EvaluateTargetHealth no puede ser true con CloudFront, y el alias solo funciona si el nombre está como alternate domain name de la distribución). Las consultas a este alias son gratis.

Comprueba que funciona

  • Peticiones normales: 200. XSS y SQLi en la query string: 403.
  • La ráfaga de peticiones acaba en 403 y get-rate-based-statement-managed-keys muestra tu IP.
  • app. responde con el primario y, con el health check invertido, con el secundario.
  • web. reparte aproximadamente 80/20.

Limpieza

Hazla completa y en este orden. Empieza por source ~/lab11.env.

  1. Route 53: borra los registros (con las mismas definiciones cambiando CREATE por DELETE), el health check y la zona:
source ~/lab11.env
sed 's/"CREATE"/"DELETE"/' failover.json > failover-del.json
sed 's/"CREATE"/"DELETE"/' weighted.json > weighted-del.json
aws route53 change-resource-record-sets --hosted-zone-id $ZONE_ID --change-batch file://failover-del.json
aws route53 change-resource-record-sets --hosted-zone-id $ZONE_ID --change-batch file://weighted-del.json
aws route53 delete-health-check --health-check-id $HC_ID
aws route53 delete-hosted-zone --id $ZONE_ID

(Si hiciste la zona privada del paso 10 o la parte C, borra también sus registros y zonas.)

  1. Desasociar WAF de la distribución (volviendo a dejar el parámetro vacío) y borrar la web ACL:
aws cloudformation deploy --stack-name lab11-cdn --template-file lab11.yaml --parameter-overrides WebAclArn=
LOCK=$(aws wafv2 get-web-acl --region us-east-1 --scope CLOUDFRONT --name lab12-acl --id $ACL_ID \
  --query LockToken --output text)
aws wafv2 delete-web-acl --region us-east-1 --scope CLOUDFRONT --name lab12-acl --id $ACL_ID --lock-token $LOCK
  1. Pila del lab 11 (bucket vacío primero):
aws s3 rm s3://$BUCKET --recursive
aws cloudformation delete-stack --stack-name lab11-cdn
aws cloudformation wait stack-delete-complete --stack-name lab11-cdn
rm -f ~/lab11.env lab11.yaml reglas.json failover*.json weighted*.json index.html hora.txt
  1. Comprueba que no queda nada:
aws route53 list-hosted-zones --query "HostedZones[?contains(Name,'lab12')].Id"
aws route53 list-health-checks --query 'HealthChecks[].Id'
aws wafv2 list-web-acls --region us-east-1 --scope CLOUDFRONT --query 'WebACLs[].Name'

Preguntas para pensar como arquitecto

1. Tu empresa tiene 60 cuentas y cada equipo crea ALB y distribuciones de CloudFront. Seguridad exige el mismo conjunto de reglas de WAF en todos, también en los que se creen mañana. ¿Cómo?

Con AWS Firewall Manager: requiere AWS Organizations y AWS Config; desde la cuenta administradora defines una política de WAF (grupos de reglas que se ejecutan primero y último en la web ACL) con ámbito por cuentas, OU o etiquetas, y Firewall Manager la aplica automáticamente a los recursos nuevos y te informa del cumplimiento.

2. Durante un ataque DDoS de capa 7, la factura se dispara por el escalado automático. ¿Qué servicio te habría protegido económicamente y qué más aporta?

AWS Shield Advanced: incluye protección de costes (créditos por el escalado causado por el ataque), acceso al Shield Response Team 24/7 (con soporte Business o Enterprise), mitigación automática de capa 7 creando reglas de WAF e incluye el uso estándar de WAF en los recursos protegidos. Cuesta 3.000 USD al mes con compromiso anual, así que solo compensa en cargas críticas.

3. El health check del paso 6 comprobaba CloudFront, no la aplicación. ¿Qué health check pondrías para un failover real entre dos regiones con ALB?

Un health check HTTPS contra una ruta de salud de la aplicación (por ejemplo /salud, que compruebe también la base de datos) de cada ALB regional, o un alias con Evaluate target health hacia cada ALB (hereda la salud de sus target groups). Para dependencias privadas, un health check basado en una alarma de CloudWatch, y para decidir con varias señales, un health check calculado.

4. ¿Por qué WAF te ha bloqueado el XSS aunque el origen es un bucket S3 que nunca ejecutaría ese script?

Porque WAF evalúa la petición en el borde, antes de que llegue al origen, sin saber qué hará el origen con ella. Es defensa en profundidad: filtra patrones maliciosos para cualquier origen (S3, ALB, API). Por eso, al añadir grupos gestionados a una aplicación existente, se empieza en modo Count para detectar falsos positivos antes de bloquear.

5. Un cliente necesita que su socio añada tus IP de servicio a su cortafuegos, y tu aplicación TCP está en dos regiones. ¿CloudFront, Route 53 o Global Accelerator?

Global Accelerator: proporciona dos IP anycast estáticas que no cambian aunque cambies de región o de endpoints, admite TCP/UDP y conmuta entre regiones con sus health checks sin depender del TTL del DNS. CloudFront usa IP que cambian y solo HTTP/HTTPS; Route 53 devuelve las IP de los endpoints, que pueden cambiar.


Volver al módulo