Al cerrar la lección anterior quedó una frase incómoda: la red de vpc-mercadofresco está bien diseñada, pero no filtra nada. La ruta local que AWS pone en todas las tablas hace que cualquier instancia de la VPC pueda intentar abrir una conexión al puerto 5432 de mercadofresco-pedidos, y la subred pública acepta lo que llegue por el Internet Gateway. El direccionamiento decide por dónde puede ir un paquete; falta decidir si se le permite pasar.

AWS ofrece dos cortafuegos para eso, y son distintos en casi todo: los grupos de seguridad, que protegen la interfaz de red de cada recurso, y las listas de control de acceso de red (NACL), que protegen el perímetro de cada subred. No compiten: se complementan, y un paquete que entra en la VPC los atraviesa los dos, en un orden concreto que conviene tener grabado.

En esta lección Marta configura el filtrado completo de MercadoFresco aplicando el patrón que distingue a una red bien hecha de una red llena de rangos IP copiados a mano: referenciar grupos de seguridad entre capas. Al terminar, la base de datos de pedidos solo aceptará conexiones del grupo de seguridad de la tienda, y ni siquiera Marta desde su portátil podrá llegar a ella directamente.

Contenido

  1. Las dos capas de filtrado de una VPC
  2. Grupos de seguridad: con estado y solo con reglas de permiso
  3. Anatomía de una regla: protocolo, puerto y origen
  4. El patrón clave: referenciar grupos de seguridad entre capas
  5. Las tres capas de MercadoFresco con sus grupos de seguridad
  6. Límites y cuotas de los grupos de seguridad
  7. NACL: sin estado, numeradas y con reglas de denegación
  8. La trampa de los puertos efímeros
  9. Tabla comparativa: grupos de seguridad frente a NACL
  10. El camino completo de un paquete
  11. Caso práctico: abrir el puerto 22 solo a la oficina (y por qué no hacerlo)
  12. Caso práctico: bloquear una IP abusiva con una NACL
  13. Depuración: qué error ves cuando falla cada cosa
  14. Confirmarlo con VPC Flow Logs
  15. Creación y auditoría por CLI
  16. Otra capa distinta: las políticas de identidad

Las dos capas de filtrado de una VPC

Antes de nada, el mapa mental. Cuando un paquete entra en la VPC desde internet, atraviesa esto:

flowchart LR
    NET["Internet"]
    NACL{{"NACL de la subred<br/>(sin estado, perímetro)"}}
    SG{{"Grupo de seguridad<br/>(con estado, en la ENI)"}}
    ENI["Instancia<br/>(su ENI)"]

    NET --> NACL --> SG --> ENI
  • La NACL actúa en la frontera de la subred. Un paquete que va de una instancia a otra dentro de la misma subred no la atraviesa.
  • El grupo de seguridad actúa en la ENI, es decir, pegado al recurso. Todo tráfico dirigido a una instancia lo atraviesa, venga de donde venga.

La consecuencia práctica es que el 95 % del trabajo diario se hace con grupos de seguridad, y las NACL se reservan para reglas gruesas de subred, sobre todo denegaciones, que es lo único que los grupos de seguridad no saben hacer.

Grupos de seguridad: con estado y solo con reglas de permiso

Un grupo de seguridad es un cortafuegos virtual que se asigna a una ENI. Tiene tres propiedades que hay que entender bien porque explican casi todo su comportamiento:

1. Solo admite reglas de permiso (allow). No existe la regla «denegar». Todo lo que no está explícitamente permitido queda denegado por defecto. Esto simplifica muchísimo el razonamiento —no hay que pensar en orden de reglas— pero significa que con un grupo de seguridad no puedes bloquear una IP concreta: para eso están las NACL.

2. Tiene estado (stateful). Si permites una conexión de entrada, la respuesta sale automáticamente, aunque no haya ninguna regla de salida que la contemple. Y al revés: si una instancia inicia una conexión de salida, la respuesta entra aunque no haya regla de entrada. El grupo de seguridad recuerda las conexiones establecidas.

Esto es lo que hace que una instancia con estas reglas funcione perfectamente:

Dirección Protocolo Puerto Origen/Destino
Entrada TCP 443 0.0.0.0/0
Salida (ninguna regla especial, la de por defecto)

Un navegador se conecta desde el puerto efímero 51234 al 443 de la instancia; la respuesta sale del 443 al 51234 sin necesitar regla, porque el grupo de seguridad sabe que esa conversación ya estaba abierta.

3. Todas las reglas se evalúan en conjunto, sin orden. Si una ENI tiene tres grupos de seguridad asignados, se evalúa la unión de todas sus reglas. Basta con que una regla de cualquiera de ellos permita el tráfico. No hay prioridades ni «la primera que coincida».

Por defecto, un grupo de seguridad recién creado tiene:

  • Entrada: ninguna regla (nada entra).
  • Salida: 0.0.0.0/0 en todos los protocolos y puertos (todo sale).

Anatomía de una regla: protocolo, puerto y origen

Cada regla tiene cuatro campos:

Campo Valores Notas
Tipo/Protocolo TCP, UDP, ICMP, o -1 (todos) En la consola se ofrecen atajos: «HTTPS», «PostgreSQL»
Rango de puertos Un puerto (443) o un rango (1024-65535) ICMP no usa puertos sino tipo y código
Origen (entrada) / Destino (salida) CIDR, otro grupo de seguridad, o prefix list El campo más importante
Descripción Texto libre Opcional pero decisivo para el mantenimiento

Los tres tipos de origen posibles:

Tipo de origen Ejemplo Cuándo usarlo
CIDR IPv4/IPv6 0.0.0.0/0, 81.45.20.7/32 Tráfico de internet o de una IP externa concreta
Otro grupo de seguridad sg-mercadofresco-alb Siempre que el origen sea otro recurso de la VPC
Prefix list pl-6da54004 (S3), o una propia Conjuntos de rangos gestionados o reutilizables

Sobre la descripción: es opcional, pero una regla sin descripción es una regla que dentro de un año nadie borrará por miedo. Marta impone la norma de que toda regla lleve una que explique por qué existe, no qué hace: «acceso del proveedor de pagos», no «puerto 443 abierto».

Un detalle sobre /32: es la máscara que designa una única dirección IP. 81.45.20.7/32 significa exactamente esa IP y ninguna otra. Es la forma correcta de expresar «solo esta máquina».

El patrón clave: referenciar grupos de seguridad entre capas

Aquí está la idea más importante de la lección. Compara dos formas de escribir la misma intención, «la base de datos solo acepta conexiones de las instancias de la tienda»:

Forma ingenua, con rangos de IP:

Entrada  TCP  5432  desde 10.0.32.0/20   # subred de app A
Entrada  TCP  5432  desde 10.0.48.0/20   # subred de app B

Forma correcta, con referencia a grupo de seguridad:

Entrada  TCP  5432  desde sg-mercadofresco-tienda

Las diferencias son de fondo, no de estilo:

Con rangos de IP Con referencia a grupo de seguridad
Alcance real Toda la subred, incluida cualquier máquina futura Solo los recursos que llevan ese grupo
Al añadir una AZ o subred Hay que editar todas las reglas No hay que tocar nada
Al escalar el ASG Funciona, pero por accidente Funciona por diseño
Al lanzar una instancia ajena en la subred Puede llegar a la base de datos No puede
Legibilidad «10.0.32.0/20» no dice nada «desde el grupo de la tienda» se lee solo

El caso que lo deja claro: si Luis lanza una instancia de pruebas en snet-mercadofresco-app-a con un grupo de seguridad distinto, con la forma ingenua esa instancia puede conectar a la base de datos de producción, porque está en el rango permitido. Con la referencia entre grupos, no puede, porque no lleva sg-mercadofresco-tienda.

Cómo funciona por dentro: cuando una regla referencia a sg-mercadofresco-tienda, AWS resuelve dinámicamente el conjunto de IP privadas de todas las ENI que tengan ese grupo asignado, y lo mantiene actualizado en tiempo real. Cuando el ASG lanza la instancia número 3 y la número 4 el viernes a las 17:00, quedan autorizadas en el mismo instante en que reciben su IP.

Dos matices que sorprenden:

  • La referencia resuelve a IP privadas. Si el tráfico llegara por la IP pública, la regla no coincidiría. Dentro de la VPC eso no ocurre, pero explica por qué hay que usar siempre nombres DNS internos.
  • Se puede referenciar el propio grupo (sg-x permite entrada desde sg-x). Es el patrón para clústeres cuyos nodos hablan entre sí, como el que necesitará ElastiCache en 06-05.

Las tres capas de MercadoFresco con sus grupos de seguridad

flowchart TB
    USER["Usuarios de internet"]

    subgraph VPC["vpc-mercadofresco"]
        subgraph PUB["Subredes públicas (10.0.0.0/20, 10.0.16.0/20)"]
            ALB["Balanceador de la tienda<br/><b>sg-mercadofresco-alb</b><br/>entrada 80 y 443 desde 0.0.0.0/0"]
        end
        subgraph APP["Subredes de aplicación (10.0.32.0/20, 10.0.48.0/20)"]
            EC2["Instancias del ASG<br/><b>sg-mercadofresco-tienda</b><br/>entrada 443 desde sg-mercadofresco-alb"]
        end
        subgraph DAT["Subredes de datos (10.0.64.0/20, 10.0.80.0/20)"]
            RDS[("mercadofresco-pedidos<br/><b>sg-mercadofresco-basedatos</b><br/>entrada 5432 desde sg-mercadofresco-tienda")]
            EFS["efs-mercadofresco-fotos<br/><b>sg-efs-mercadofresco</b><br/>entrada 2049 desde sg-mercadofresco-tienda"]
        end
    end

    USER -->|"443"| ALB
    ALB -->|"443"| EC2
    EC2 -->|"5432"| RDS
    EC2 -->|"2049 NFS"| EFS

La cadena de referencias es la parte elegante: nadie escribe un solo rango IP privado. Solo el grupo del balanceador, que es la puerta de entrada, menciona 0.0.0.0/0.

Las reglas completas, grupo por grupo:

sg-mercadofresco-alb — la puerta de entrada

Dir. Protocolo Puerto Origen/Destino Descripción
Entrada TCP 443 0.0.0.0/0 Tráfico HTTPS público de la tienda
Entrada TCP 80 0.0.0.0/0 Solo para redirigir a HTTPS (ver 03-03)
Salida TCP 443 sg-mercadofresco-tienda Reenvío a las instancias del ASG

sg-mercadofresco-tienda — la capa de aplicación

Dir. Protocolo Puerto Origen/Destino Descripción
Entrada TCP 443 sg-mercadofresco-alb Solo desde el balanceador
Salida TCP 5432 sg-mercadofresco-basedatos Consultas a la base de pedidos
Salida TCP 2049 sg-efs-mercadofresco Montaje NFS del sistema de ficheros
Salida TCP 443 0.0.0.0/0 API de AWS, pasarela de pago, actualizaciones

Fíjate en que no hay regla de entrada para el puerto 22. Ni una IP de oficina, ni nada. Volvemos a ello en el caso práctico.

sg-mercadofresco-basedatos — la capa de datos

Dir. Protocolo Puerto Origen/Destino Descripción
Entrada TCP 5432 sg-mercadofresco-tienda Consultas de la aplicación
Entrada TCP 5432 sg-lambda-mercadofresco Función mercadofresco-estado-pedido
Salida (ninguna) Una base de datos no necesita salir

Ese «ninguna salida» es deliberado: hay que borrar explícitamente la regla de salida por defecto 0.0.0.0/0. Como el grupo tiene estado, las respuestas a las consultas salen igualmente.

sg-efs-mercadofresco — el sistema de ficheros compartido

Dir. Protocolo Puerto Origen/Destino Descripción
Entrada TCP 2049 sg-mercadofresco-tienda NFS desde las instancias de la tienda

Límites y cuotas de los grupos de seguridad

Números que conviene tener presentes porque se alcanzan antes de lo que parece:

Límite Valor por defecto Ampliable
Grupos de seguridad por VPC 2.500
Reglas de entrada por grupo 60 Sí, hasta 1.000
Reglas de salida por grupo 60
Grupos de seguridad por ENI 5 Sí, hasta 16
Reglas × grupos por ENI 1.000

La regla real es la última: reglas por grupo × grupos por ENI no puede pasar de 1.000. Y un detalle contable: una regla con un rango de puertos cuenta como una, pero una regla con varios CIDR cuenta como una por CIDR. Un grupo con «443 desde estas 40 IP» consume 40 reglas. Es exactamente el escenario para el que existen las prefix lists gestionadas por el cliente: se define una lista con las 40 IP y se referencia con una sola regla.

NACL: sin estado, numeradas y con reglas de denegación

Una NACL (Network Access Control List) es un cortafuegos a nivel de subred. Cada subred está asociada a exactamente una NACL; si no la asocias, usa la NACL por defecto de la VPC, que permite absolutamente todo en ambos sentidos.

Sus propiedades son casi las opuestas a las de un grupo de seguridad:

1. Permite y deniega. Cada regla es allow o deny. Esto es lo único que las hace imprescindibles.

2. No tiene estado (stateless). No recuerda nada. Si una petición entra por una regla de entrada, la respuesta necesita su propia regla de salida. Este es el origen del 90 % de los problemas con NACL.

3. Se evalúan en orden numérico, y la primera coincidencia gana. Las reglas se numeran de 1 a 32.766. AWS las recorre de menor a mayor y se detiene en la primera que coincida, ignorando todas las siguientes.

4. Existe la regla *. Al final de toda NACL hay una regla no editable, numerada *, que deniega todo lo que no haya coincidido antes. Es la red de seguridad final.

La NACL por defecto de una VPC nueva:

Regla Tipo Protocolo Puerto Origen Acción
100 Todo Todos Todos 0.0.0.0/0 ALLOW
* Todo Todos Todos 0.0.0.0/0 DENY

(y lo mismo en salida). Es decir: por defecto, una NACL no filtra nada. Esto es intencionado, y es la razón por la que muchas arquitecturas correctas dejan las NACL tal cual y confían todo el filtrado a los grupos de seguridad.

Consejo de numeración: usa saltos de 100 (100, 200, 300…). Cuando necesites insertar una regla entre dos, tendrás sitio. Renumerar una NACL en producción es una operación desagradable.

La trampa de los puertos efímeros

Este apartado merece su propia sección porque es donde todo el mundo se estrella.

Cuando un navegador se conecta a la tienda, abre la conexión desde un puerto aleatorio alto —un puerto efímero— hacia el 443 del servidor. El servidor responde desde el 443 hacia ese puerto efímero. Como la NACL no tiene estado, esa respuesta se evalúa contra las reglas de salida, y su puerto de destino no es el 443: es el 51234, o el 62890, o el que sea.

Por eso una NACL restrictiva necesita siempre una regla de salida que abra el rango de puertos efímeros. Y el rango depende del cliente:

Sistema del cliente Rango de puertos efímeros
Linux moderno 32768–60999
Windows (desde Vista/2008) 49152–65535
Balanceadores de AWS (ELB/NLB) 1024–65535
Lambda, contenedores 1024–65535

La recomendación práctica de AWS: abrir 1024–65535 en salida, porque no sabes qué cliente tendrás. Sí, es un rango enorme, y es la razón por la que las NACL son un instrumento tosco: filtrar finamente el tráfico de vuelta es prácticamente imposible.

Una NACL funcional para una subred pública queda así:

Regla Dir. Protocolo Puerto Origen/Destino Acción Por qué
100 Entrada TCP 443 0.0.0.0/0 ALLOW Peticiones HTTPS
110 Entrada TCP 80 0.0.0.0/0 ALLOW Peticiones HTTP a redirigir
120 Entrada TCP 1024-65535 0.0.0.0/0 ALLOW Respuestas a conexiones salientes
* Entrada Todos Todos 0.0.0.0/0 DENY Regla implícita
100 Salida TCP 443 0.0.0.0/0 ALLOW Conexiones salientes HTTPS
110 Salida TCP 1024-65535 0.0.0.0/0 ALLOW Respuestas a las peticiones entrantes
* Salida Todos Todos 0.0.0.0/0 DENY Regla implícita

Cuatro de las seis reglas existen solo por la falta de estado. Compáralo con el grupo de seguridad equivalente, que necesitaba una.

Tabla comparativa: grupos de seguridad frente a NACL

Criterio Grupo de seguridad NACL
Nivel ENI (instancia, RDS, ALB, endpoint…) Subred completa
Estado Con estado: la respuesta se permite sola Sin estado: la respuesta necesita regla propia
Tipos de regla Solo permitir Permitir y denegar
Evaluación Todas las reglas a la vez, sin orden Por número, la primera que coincide gana
Cuántas se aplican Varias por ENI (hasta 5), se suman Una por subred
Por defecto Nada entra, todo sale Todo entra y todo sale
Origen por grupo de seguridad , la característica clave No, solo CIDR
Tráfico dentro de la misma subred Sí lo filtra No lo ve
Cuándo usarlo Siempre; es la herramienta principal Bloqueos gruesos por subred y denegaciones
Caso de uso típico «La base de datos solo habla con la tienda» «Esta IP no entra en la subred pública»

Regla práctica que resume el reparto: los grupos de seguridad definen la arquitectura permitida; las NACL bloquean lo que hay que bloquear.

El camino completo de un paquete

Este diagrama es el que hay que memorizar. Sigue una petición HTTPS de un cliente hasta la instancia y su respuesta de vuelta:

sequenceDiagram
    participant C as Cliente<br/>(203.0.113.50:51234)
    participant NE as NACL entrada<br/>subred app
    participant SE as SG entrada<br/>sg-mercadofresco-tienda
    participant I as Instancia<br/>(10.0.32.15:443)
    participant SS as SG salida<br/>(con estado)
    participant NS as NACL salida<br/>subred app

    C->>NE: Petición → puerto destino 443
    Note over NE: Regla 100: ALLOW TCP 443 ✔
    NE->>SE: Petición
    Note over SE: Entrada 443 desde sg-alb ✔
    SE->>I: Llega a la aplicación
    I->>SS: Respuesta → puerto destino 51234
    Note over SS: Conexión ya establecida:<br/>NO se evalúa ninguna regla ✔
    SS->>NS: Respuesta
    Note over NS: Regla 110: ALLOW TCP 1024-65535<br/>SIN esta regla, la respuesta MUERE AQUÍ ✘
    NS->>C: Respuesta entregada

Cuatro puntos de control en total, y el asimétrico es el tercero: el grupo de seguridad no evalúa nada en la salida porque recuerda la conexión, mientras que la NACL la vuelve a examinar como si fuera tráfico nuevo. El síntoma cuando falta la regla 110 es especialmente cruel: la conexión se establece, la petición llega, la aplicación la procesa correctamente… y el cliente se queda esperando hasta que expira el tiempo de espera.

Caso práctico: abrir el puerto 22 solo a la oficina (y por qué no hacerlo)

Luis pide acceso SSH a las instancias para depurar. La respuesta «correcta según el manual» sería abrir el 22 solo a la IP fija de la oficina:

aws ec2 authorize-security-group-ingress \
  --profile mercadofresco-dev --region eu-west-1 \
  --group-id "$SG_TIENDA" \
  --ip-permissions 'IpProtocol=tcp,FromPort=22,ToPort=22,IpRanges=[{CidrIp=81.45.20.7/32,Description="SSH desde la oficina de Barcelona"}]'

Nunca 0.0.0.0/0. Un puerto 22 abierto a internet recibe intentos de acceso automatizados en cuestión de minutos.

Pero para MercadoFresco ni siquiera esto es la solución correcta, por cuatro razones:

  1. La IP de la oficina cambia. El día que el operador la renueve, nadie entra, y alguien «arreglará» el problema poniendo 0.0.0.0/0 a las once de la noche.
  2. No sirve para el teletrabajo. Marta desde casa tiene otra IP.
  3. No queda registro de quién entró. SSH con clave compartida no identifica a la persona.
  4. Las instancias del ASG son efímeras. Entrar por SSH a una máquina que se destruirá en dos horas para «arreglarla» a mano es exactamente lo que el ASG intenta evitar.

La alternativa, que ya usamos en 02-01, es Session Manager: el agente de SSM instalado en la instancia inicia una conexión saliente hacia el servicio de AWS, y la sesión viaja por ese túnel.

SSH con puerto 22 abierto Session Manager
Puertos de entrada necesarios 22 desde algún origen Ninguno
Necesita IP pública o bastión No
Gestión de claves Ficheros .pem que se comparten Ninguna: usa credenciales de AWS
Quién entró y qué hizo Sin registro fiable Registrado en CloudTrail y CloudWatch Logs
Control de permisos Todo o nada Políticas de IAM por persona (04-01)
Funciona en subred privada Solo con bastión , con NAT o con endpoints de interfaz

Por eso sg-mercadofresco-tienda no tiene ninguna regla de entrada para el 22, y esa ausencia es una decisión de diseño, no un olvido.

Caso práctico: bloquear una IP abusiva con una NACL

Un martes, Sara detecta en los registros que la IP 198.51.100.77 está recorriendo el catálogo entero a razón de 40 peticiones por segundo. No es un ataque distribuido, es una sola IP raspando precios. Los grupos de seguridad no sirven: no saben denegar.

# Localizar la NACL asociada a las subredes públicas
NACL_PUB=$(aws ec2 describe-network-acls \
  --profile mercadofresco-dev --region eu-west-1 \
  --filters "Name=association.subnet-id,Values=$PUB_A" \
  --query 'NetworkAcls[0].NetworkAclId' --output text)

# Regla 50: número BAJO para que se evalúe ANTES que el ALLOW general de la 100
aws ec2 create-network-acl-entry \
  --profile mercadofresco-dev --region eu-west-1 \
  --network-acl-id "$NACL_PUB" \
  --rule-number 50 \
  --protocol -1 \
  --rule-action deny \
  --ingress \
  --cidr-block 198.51.100.77/32

El número 50 es toda la lección: si hubiéramos usado el 150, la regla 100 (ALLOW general) se evaluaría primero, coincidiría, y la 150 nunca se leería. La primera coincidencia gana.

Para retirarla cuando el abuso cese:

aws ec2 delete-network-acl-entry --profile mercadofresco-dev --region eu-west-1 \
  --network-acl-id "$NACL_PUB" --rule-number 50 --ingress

Dos advertencias honestas:

  • Bloquear IP a mano no escala. Si mañana son 300 IP, esto es inviable y hará falta la herramienta adecuada: AWS WAF, que se estudia en 04-05, con sus reglas de límite de tasa. La NACL es la respuesta de urgencia, no la solución.
  • La NACL bloquea al entrar en la subred. Si el tráfico llega a través de CloudFront (lección 03-04) o de un balanceador, la IP que ve la NACL es la del intermediario, no la del atacante, y bloquearla dejaría fuera a todo el mundo.

Depuración: qué error ves cuando falla cada cosa

Los síntomas son distintos y saber leerlos ahorra horas:

Síntoma Causa más probable Cómo confirmarlo
Tiempo de espera agotado (connection timed out, la petición se queda colgada) Grupo de seguridad o NACL: el paquete se descarta en silencio Flow Logs con REJECT, o ninguna entrada en absoluto
Conexión rechazada (connection refused, respuesta inmediata) La red funciona: no hay nada escuchando en ese puerto ss -lntp en la instancia; el servicio está caído
Se conecta y se cuelga a mitad NACL sin regla de puertos efímeros en salida Flow Logs: ACCEPT de entrada y REJECT de salida
Se resuelve a una IP pública desde dentro enableDnsHostnames o PubliclyAccessible dig desde la instancia (visto en 03-01)
Funciona desde una instancia y no desde otra La segunda no lleva el grupo de seguridad referenciado describe-instances y comparar SecurityGroups
Acceso denegado (AccessDenied) de la API de AWS No es la red: es una política de IAM CloudTrail; se estudia en 04-01

La distinción entre las dos primeras filas es la más útil de toda la tabla: un cortafuegos descarta el paquete en silencio y el cliente espera; un servicio caído responde inmediatamente rechazando. Si telnet mercadofresco-pedidos... 5432 devuelve «connection refused» al instante, el problema no está en el grupo de seguridad.

Confirmarlo con VPC Flow Logs

Los Flow Logs que activamos en 03-01 son la única forma de tener certeza. Recordando el formato:

2 111122223333 eni-0a1b2c3d 10.0.32.15 10.0.64.30 44321 5432 6 12 3800 1738500000 1738500060 REJECT OK

Aquí 10.0.32.15 (instancia de la tienda) intentó llegar a 10.0.64.30 (la base de datos) en el puerto 5432, y el resultado fue REJECT. Eso confirma que el bloqueo ocurre en la capa de red.

Cómo distinguir cuál de las dos capas ha bloqueado:

Qué ves en los Flow Logs Interpretación
Solo un REJECT de entrada, ninguna respuesta Grupo de seguridad de destino: descartó la petición
ACCEPT de entrada y REJECT de salida en la respuesta NACL: falta la regla de puertos efímeros
Ninguna entrada en absoluto El paquete ni llegó: problema de rutas (03-01) o de DNS
ACCEPT en ambos sentidos pero la aplicación falla La red está bien: mira la aplicación

Esa tercera fila es especialmente valiosa: la ausencia de registros es información. El análisis con CloudWatch Logs Insights y las consultas sobre estos datos se estudian en 05-01.

Creación y auditoría por CLI

Crear los grupos con referencias cruzadas

Hay un problema de huevo y gallina: sg-mercadofresco-tienda referencia a sg-mercadofresco-alb y al revés. La solución es crear primero los grupos vacíos y después las reglas.

crear_sg() {
  local nombre=$1 desc=$2 componente=$3
  aws ec2 create-security-group \
    --profile mercadofresco-dev --region eu-west-1 \
    --group-name "$nombre" --description "$desc" --vpc-id "$VPC_ID" \
    --tag-specifications "ResourceType=security-group,Tags=[
        {Key=Name,Value=$nombre},{Key=Proyecto,Value=mercadofresco},
        {Key=Entorno,Value=produccion},{Key=Componente,Value=$componente},
        {Key=Propietario,Value=marta},{Key=CentroCoste,Value=operaciones}]" \
    --query 'GroupId' --output text
}

SG_ALB=$(crear_sg   sg-mercadofresco-alb        "Balanceador publico de la tienda" tienda)
SG_TIENDA=$(crear_sg sg-mercadofresco-tienda    "Instancias EC2 de la tienda"      tienda)
SG_BD=$(crear_sg    sg-mercadofresco-basedatos  "RDS PostgreSQL de pedidos"        pedidos)
SG_EFS=$(crear_sg   sg-efs-mercadofresco        "Sistema de ficheros de fotos"     catalogo)

Ahora las reglas. --ip-permissions en formato largo permite incluir la descripción, cosa que la forma corta --port / --cidr no admite:

# ALB: entrada desde internet
aws ec2 authorize-security-group-ingress --profile mercadofresco-dev --region eu-west-1 \
  --group-id "$SG_ALB" --ip-permissions \
  'IpProtocol=tcp,FromPort=443,ToPort=443,IpRanges=[{CidrIp=0.0.0.0/0,Description="HTTPS publico de la tienda"}]' \
  'IpProtocol=tcp,FromPort=80,ToPort=80,IpRanges=[{CidrIp=0.0.0.0/0,Description="HTTP para redirigir a HTTPS"}]'

# Tienda: entrada SOLO desde el grupo del balanceador (UserIdGroupPairs, no IpRanges)
aws ec2 authorize-security-group-ingress --profile mercadofresco-dev --region eu-west-1 \
  --group-id "$SG_TIENDA" --ip-permissions \
  "IpProtocol=tcp,FromPort=443,ToPort=443,UserIdGroupPairs=[{GroupId=$SG_ALB,Description=\"Trafico del balanceador\"}]"

# Base de datos: entrada SOLO desde el grupo de la tienda
aws ec2 authorize-security-group-ingress --profile mercadofresco-dev --region eu-west-1 \
  --group-id "$SG_BD" --ip-permissions \
  "IpProtocol=tcp,FromPort=5432,ToPort=5432,UserIdGroupPairs=[{GroupId=$SG_TIENDA,Description=\"Consultas de la tienda\"}]"

# EFS: NFS desde la tienda
aws ec2 authorize-security-group-ingress --profile mercadofresco-dev --region eu-west-1 \
  --group-id "$SG_EFS" --ip-permissions \
  "IpProtocol=tcp,FromPort=2049,ToPort=2049,UserIdGroupPairs=[{GroupId=$SG_TIENDA,Description=\"Montaje NFS de la tienda\"}]"

# Quitar la salida total de la base de datos: no necesita iniciar nada
aws ec2 revoke-security-group-egress --profile mercadofresco-dev --region eu-west-1 \
  --group-id "$SG_BD" --ip-permissions 'IpProtocol=-1,IpRanges=[{CidrIp=0.0.0.0/0}]'

UserIdGroupPairs es la clave del patrón: es el campo que expresa «desde este otro grupo de seguridad» en lugar de un rango de IP.

Auditoría: encontrar reglas peligrosas

Este comando es el que Marta ejecuta cada lunes. Busca cualquier grupo de seguridad de la cuenta con un puerto de administración o de base de datos abierto a todo internet:

aws ec2 describe-security-groups --profile mercadofresco-dev --region eu-west-1 \
  --query 'SecurityGroups[?IpPermissions[?
      (FromPort==`22` || FromPort==`3389` || FromPort==`3306` || FromPort==`5432` || FromPort==`6379`)
      && IpRanges[?CidrIp==`0.0.0.0/0`]]].{
        Grupo:GroupName, Id:GroupId, VPC:VpcId,
        Puertos:IpPermissions[?IpRanges[?CidrIp==`0.0.0.0/0`]].FromPort}' \
  --output table

Desglose de la expresión JMESPath, que es densa pero merece la pena entender:

  • SecurityGroups[? ... ] filtra la lista de grupos.
  • IpPermissions[? ... ] mira dentro de las reglas de entrada de cada grupo.
  • La condición combina dos cosas con &&: que el puerto sea uno de los sensibles (22 SSH, 3389 RDP, 3306 MySQL, 5432 PostgreSQL, 6379 Redis) y que entre sus rangos haya un 0.0.0.0/0.
  • Los acentos graves (`) marcan literales numéricos y de cadena en JMESPath, como vimos en 01-05.

Si esto devuelve algo, hay un problema que arreglar hoy. Y un complemento útil, buscar grupos huérfanos que nadie usa:

# Grupos que no están asignados a ninguna interfaz de red
comm -23 \
  <(aws ec2 describe-security-groups --profile mercadofresco-dev --region eu-west-1 \
      --query 'SecurityGroups[].GroupId' --output text | tr '\t' '\n' | sort) \
  <(aws ec2 describe-network-interfaces --profile mercadofresco-dev --region eu-west-1 \
      --query 'NetworkInterfaces[].Groups[].GroupId' --output text | tr '\t' '\n' | sort -u)

Y la comprobación desde Python, útil para integrarla en un pipeline (módulo 8):

import boto3

ec2 = boto3.client("ec2", region_name="eu-west-1")
PUERTOS_SENSIBLES = {22, 3389, 3306, 5432, 6379, 27017}

hallazgos = []
paginador = ec2.get_paginator("describe_security_groups")

for pagina in paginador.paginate():
    for grupo in pagina["SecurityGroups"]:
        for regla in grupo.get("IpPermissions", []):
            # Una regla con IpProtocol == "-1" no lleva FromPort: son TODOS los puertos
            desde = regla.get("FromPort", 0)
            hasta = regla.get("ToPort", 65535)
            abierto_a_internet = any(
                r["CidrIp"] == "0.0.0.0/0" for r in regla.get("IpRanges", [])
            )
            if not abierto_a_internet:
                continue
            afectados = {p for p in PUERTOS_SENSIBLES if desde <= p <= hasta}
            if afectados:
                hallazgos.append({
                    "grupo": grupo["GroupName"],
                    "id": grupo["GroupId"],
                    "puertos": sorted(afectados),
                })

for h in hallazgos:
    print(f"PELIGRO  {h['grupo']:35} {h['id']:22} puertos {h['puertos']}")

print(f"\n{len(hallazgos)} reglas peligrosas encontradas")

Detalle importante del código: se comprueba el rango completo (desde <= p <= hasta), no solo el puerto inicial. Una regla «TCP 1-65535 desde 0.0.0.0/0» no tiene FromPort == 22, pero incluye el 22 y es igual de peligrosa. El comando CLI anterior, más simple, se le escaparía.

Otra capa distinta: las políticas de identidad

Conviene cerrar deshaciendo una confusión frecuente. Los grupos de seguridad y las NACL controlan paquetes de red: quién puede abrir una conexión TCP a qué puerto. No tienen ninguna opinión sobre quién es el que hace la petición ni sobre qué acción solicita.

Cuando la instancia de la tienda pide un objeto de mercadofresco-catalogo-fotos, hay dos preguntas independientes:

  1. ¿Hay camino y está permitido el paquete? → rutas (03-01), grupo de seguridad, NACL, endpoint de VPC.
  2. ¿Tiene permiso esa identidad para hacer s3:GetObject sobre ese objeto?políticas de IAM, el rol rol-mercadofresco-tienda, y las políticas de bucket. Eso es materia de la lección 04-01, y un AccessDenied de ahí no se arregla nunca tocando un grupo de seguridad.

Distinguir estas dos capas ahorra sesiones enteras de depuración en la dirección equivocada. Y la protección frente a ataques a nivel de aplicación —inyecciones, bots, denegación de servicio— es una tercera capa distinta todavía, la de Shield (04-04) y WAF (04-05).

Errores Comunes y Consejos

Usar rangos de IP donde debería haber referencias entre grupos. Funciona, y por eso se propaga. Pero abre la puerta a cualquier recurso futuro de esas subredes y obliga a editar reglas cada vez que cambia la red. Si el origen es un recurso de la VPC, la respuesta es siempre UserIdGroupPairs.

Abrir el 22 o el 3389 a 0.0.0.0/0 «un momento, para probar». Los escáneres automáticos encuentran el puerto en minutos. Si de verdad hace falta, /32 de una IP concreta; mejor, Session Manager.

Olvidar los puertos efímeros en la NACL de salida. El síntoma —la conexión se establece y luego se cuelga— no apunta a la NACL en absoluto. Si tocas una NACL, abre 1024-65535 en salida.

Numerar mal las reglas de NACL. Un DENY con número mayor que un ALLOW que ya coincide es una regla muerta. Las denegaciones van con números bajos. Y numera de 100 en 100 para poder insertar.

Creer que una NACL filtra el tráfico entre dos instancias de la misma subred. No lo hace: la NACL solo actúa en la frontera de la subred. Ese tráfico solo lo ven los grupos de seguridad.

Dejar la salida 0.0.0.0/0 en el grupo de la base de datos. No es un riesgo inmediato, pero si la base de datos se ve comprometida, esa regla es su vía para sacar datos. Quítala: el estado del grupo de seguridad hace que las respuestas salgan igual.

Confundir «tiempo de espera agotado» con «conexión rechazada». El primero es un cortafuegos, el segundo es un servicio caído. Diagnosticar en la dirección equivocada cuesta horas.

No poner descripción en las reglas. Una regla sin explicación es permanente por miedo. Exige una descripción en cada regla y revisa la lista cada trimestre.

Consejo de oro: dibuja el diagrama de las capas con las flechas de los grupos de seguridad antes de escribir una sola regla. Si la flecha no está en el dibujo, la regla no debería existir.

Ejercicios

Ejercicio 1: diseñar los grupos de seguridad de un panel interno

MercadoFresco necesita un panel de administración interno en instancias propias, que Sara usará desde la oficina. El panel debe: recibir tráfico del balanceador en el 443, consultar la réplica de lectura mercadofresco-pedidos-lectura en el 5432, y escribir informes en mercadofresco-informes-analitica. No debe poder alcanzar la base de datos primaria.

Diseña el grupo sg-mercadofresco-panel y los cambios necesarios en los grupos existentes, respetando el patrón de referencias.

Ejercicio 2: corregir una NACL rota

Un compañero ha configurado esta NACL en la subred de aplicación y la tienda ha dejado de responder:

Regla Dir. Protocolo Puerto Origen Acción
100 Entrada TCP 443 10.0.0.0/16 ALLOW
* Entrada Todos Todos 0.0.0.0/0 DENY
100 Salida TCP 5432 10.0.64.0/20 ALLOW
200 Salida TCP 443 0.0.0.0/0 ALLOW
* Salida Todos Todos 0.0.0.0/0 DENY

Identifica todos los problemas, explica el síntoma exacto de cada uno y escribe la NACL corregida.

Ejercicio 3: auditar y corregir un grupo de seguridad heredado

Escribe un script de shell que, para un grupo de seguridad dado, imprima un informe legible de todas sus reglas de entrada distinguiendo el tipo de origen, marque las peligrosas, y genere los comandos revoke-security-group-ingress necesarios para eliminarlas sin ejecutarlos.

Soluciones

Solución 1

Grupo nuevo sg-mercadofresco-panel:

Dir. Protocolo Puerto Origen/Destino Descripción
Entrada TCP 443 sg-mercadofresco-alb Panel servido a través del balanceador
Salida TCP 5432 sg-mercadofresco-lectura Consultas a la réplica de lectura
Salida TCP 443 0.0.0.0/0 S3 y API de AWS

Cambios en los grupos existentes: hace falta un grupo nuevo para la réplica, sg-mercadofresco-lectura, separado de sg-mercadofresco-basedatos. Ese es el núcleo del ejercicio: si la réplica compartiera grupo con la primaria, autorizar al panel en la réplica lo autorizaría también en la primaria, incumpliendo el requisito.

sg-mercadofresco-lectura Protocolo Puerto Origen
Entrada TCP 5432 sg-mercadofresco-panel
Entrada TCP 5432 sg-mercadofresco-tienda

Y sg-mercadofresco-basedatos no se toca: sigue aceptando solo desde sg-mercadofresco-tienda y la Lambda, por lo que el panel no puede llegar a la primaria.

Sobre el acceso de Sara desde la oficina: no se abre ningún puerto para ella. Entra por el mismo balanceador que los clientes, y la restricción a la oficina se hace por autenticación de la aplicación, o con WAF filtrando por IP en /admin (04-05). Añadir una regla con la IP de la oficina al grupo del panel sería volver al problema de las IP cambiantes.

Sobre el acceso a mercadofresco-informes-analitica: no requiere ninguna regla nueva más allá del 443 de salida, y si existe el endpoint de puerta de enlace de S3 (03-01), ni siquiera pasa por el NAT. El permiso para escribir en el bucket es cosa del rol de IAM (04-01), no del cortafuegos.

Solución 2

Hay tres problemas:

Problema 1 — falta el rango de puertos efímeros en la salida. Las peticiones HTTPS entran por la regla 100 y llegan a la aplicación, pero la respuesta va dirigida al puerto efímero del cliente (por ejemplo, 51234). En salida, la 100 solo cubre el 5432 y la 200 solo el 443, así que la respuesta cae en el * DENY. Síntoma: la conexión se establece, el servidor procesa la petición y el cliente se queda colgado hasta que expira el tiempo de espera. En los Flow Logs: ACCEPT de entrada y REJECT de salida.

Problema 2 — falta el rango de puertos efímeros en la entrada. Cuando la instancia consulta a RDS (saliendo por el 5432, permitido por la regla 100 de salida), la respuesta de RDS llega a un puerto efímero de la instancia. En entrada solo está permitido el 443, así que la respuesta se deniega. Síntoma: las consultas a la base de datos se cuelgan y acaban en tiempo de espera agotado. Lo mismo ocurre con las llamadas HTTPS salientes a S3 o a la pasarela de pago.

Problema 3 — el origen de la entrada es demasiado restrictivo en un sentido y poco preciso en otro. 10.0.0.0/16 cubre la VPC entera, lo cual es correcto si el tráfico solo viene del balanceador, pero además permite que cualquier subred de la VPC entre por el 443, cosa que la NACL no debería decidir. No rompe nada, pero es una regla mal planteada: el filtrado fino de origen es trabajo del grupo de seguridad.

NACL corregida:

Regla Dir. Protocolo Puerto Origen/Destino Acción Por qué
100 Entrada TCP 443 10.0.0.0/16 ALLOW Peticiones del balanceador
200 Entrada TCP 1024-65535 0.0.0.0/0 ALLOW Respuestas a consultas salientes
* Entrada Todos Todos 0.0.0.0/0 DENY Implícita
100 Salida TCP 5432 10.0.64.0/20 ALLOW Consultas a RDS
200 Salida TCP 443 0.0.0.0/0 ALLOW S3, API de AWS, pagos
300 Salida TCP 1024-65535 0.0.0.0/0 ALLOW Respuestas al balanceador
* Salida Todos Todos 0.0.0.0/0 DENY Implícita

Moraleja del ejercicio: si el filtrado fino ya lo hacen los grupos de seguridad, esta NACL podría perfectamente dejarse en el ALLOW total por defecto sin perder seguridad real, y se habría evitado la incidencia.

Solución 3

#!/usr/bin/env bash
# auditar-sg.sh — informe de reglas de entrada de un grupo de seguridad
set -euo pipefail

GRUPO="${1:?Uso: $0 <sg-id>}"
PERFIL="mercadofresco-dev"
REGION="eu-west-1"
PELIGROSOS="22 3389 3306 5432 6379 27017"

echo "=== Reglas de entrada de $GRUPO ==="

REGLAS=$(aws ec2 describe-security-groups --profile "$PERFIL" --region "$REGION" \
  --group-ids "$GRUPO" --query 'SecurityGroups[0].IpPermissions' --output json)

# Recorremos las reglas con jq, generando una línea por combinación regla-origen
echo "$REGLAS" | jq -r '
  .[] |
  . as $r |
  (($r.IpRanges // [])[]      | "CIDR|\($r.IpProtocol)|\($r.FromPort // 0)|\($r.ToPort // 65535)|\(.CidrIp)|\(.Description // "-")"),
  (($r.UserIdGroupPairs // [])[] | "SG  |\($r.IpProtocol)|\($r.FromPort // 0)|\($r.ToPort // 65535)|\(.GroupId)|\(.Description // "-")"),
  (($r.PrefixListIds // [])[]   | "PL  |\($r.IpProtocol)|\($r.FromPort // 0)|\($r.ToPort // 65535)|\(.PrefixListId)|\(.Description // "-")")
' | while IFS='|' read -r tipo proto desde hasta origen desc; do

    marca="  "
    if [[ "$origen" == "0.0.0.0/0" ]]; then
      for p in $PELIGROSOS; do
        if (( desde <= p && p <= hasta )); then marca="!!"; break; fi
      done
    fi

    printf "%s %-4s %-5s %6s-%-6s %-22s %s\n" \
      "$marca" "$tipo" "$proto" "$desde" "$hasta" "$origen" "$desc"

    if [[ "$marca" == "!!" ]]; then
      echo "     → aws ec2 revoke-security-group-ingress --profile $PERFIL --region $REGION \\"
      echo "         --group-id $GRUPO --protocol $proto --port $desde-$hasta --cidr $origen"
    fi
done

echo
echo "Las líneas marcadas con !! exponen un puerto sensible a todo internet."
echo "Revisa los comandos sugeridos antes de ejecutarlos."

Puntos de diseño del script: distingue los tres tipos de origen (CIDR, SG, PL) porque una referencia a otro grupo nunca es peligrosa por sí misma; comprueba el rango completo de puertos en lugar de solo el inicial; usa // 0 y // 65535 en jq para el caso IpProtocol: "-1", donde FromPort y ToPort no existen; y no ejecuta nada, solo imprime los comandos, porque revocar reglas a ciegas en producción es peor que la vulnerabilidad.

Conclusión

MercadoFresco ya tiene cortafuegos. Sabes que los grupos de seguridad actúan en la ENI, tienen estado —la respuesta a una conexión permitida sale sola, sin regla—, solo permiten y se evalúan en conjunto sin orden, y que por defecto no dejan entrar nada y lo dejan salir todo. Conoces la anatomía de una regla y, sobre todo, dominas el patrón que define una red bien construida: referenciar unos grupos de seguridad desde otros con UserIdGroupPairs en lugar de escribir rangos de IP. Gracias a eso, sg-mercadofresco-basedatos acepta el 5432 solo desde sg-mercadofresco-tienda, y las instancias que el ASG lanza el viernes a las 17:00 quedan autorizadas en el mismo instante en que arrancan, sin editar nada.

Sabes que las NACL son lo contrario en casi todo: actúan en la frontera de la subred, no tienen estado, se evalúan por número deteniéndose en la primera coincidencia, terminan siempre en la regla * de denegación, y son la única de las dos capas capaz de denegar. Has entendido por qué eso las obliga a abrir el rango de puertos efímeros 1024-65535 en el sentido de vuelta, y por qué el síntoma de olvidarlo —la conexión se establece y se cuelga— es tan difícil de atribuir. Has recorrido el camino completo de un paquete por los cuatro puntos de control y sabes cuál de ellos es asimétrico y por qué.

En la práctica has decidido no abrir el puerto 22 ni siquiera a la IP de la oficina, con cuatro razones concretas y Session Manager como alternativa; has bloqueado una IP abusiva con una regla DENY numerada 50, entendiendo que el número bajo es lo que la hace efectiva, y sabiendo que esa es una respuesta de urgencia y no una solución que escale —para eso está WAF, en 04-05. Sabes distinguir un tiempo de espera agotado (cortafuegos) de una conexión rechazada (servicio caído), y confirmar cuál de las dos capas ha bloqueado un paquete leyendo el ACCEPT/REJECT de los VPC Flow Logs. Y has automatizado la auditoría semanal que busca puertos sensibles abiertos a 0.0.0.0/0, tanto en JMESPath como en boto3 comprobando el rango completo de puertos. Por último, tienes claro que las políticas de identidad son una capa distinta: un AccessDenied de IAM nunca se arregla tocando un grupo de seguridad, y eso es materia del módulo 4.

Con la red diseñada y filtrada, ya podemos poner la pieza que MercadoFresco lleva esperando desde la primera lección. El grupo de Auto Scaling asg-mercadofresco-tienda sabe lanzar cuatro instancias el viernes por la tarde, pero nada reparte el tráfico entre ellas: hoy siguen colgando de una única IP. En la lección 03-03, «Elastic Load Balancing», montaremos el balanceador de aplicación en las subredes públicas con el grupo sg-mercadofresco-alb que acabamos de crear, lo conectaremos al ASG para que registre y dé de baja las instancias automáticamente, configuraremos comprobaciones de estado que no tumben la aplicación en lugar de salvarla, y terminaremos de resolver el problema 1: las caídas de los viernes.

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