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
- Las dos capas de filtrado de una VPC
- Grupos de seguridad: con estado y solo con reglas de permiso
- Anatomía de una regla: protocolo, puerto y origen
- El patrón clave: referenciar grupos de seguridad entre capas
- Las tres capas de MercadoFresco con sus grupos de seguridad
- Límites y cuotas de los grupos de seguridad
- NACL: sin estado, numeradas y con reglas de denegación
- La trampa de los puertos efímeros
- Tabla comparativa: grupos de seguridad frente a NACL
- El camino completo de un paquete
- Caso práctico: abrir el puerto 22 solo a la oficina (y por qué no hacerlo)
- Caso práctico: bloquear una IP abusiva con una NACL
- Depuración: qué error ves cuando falla cada cosa
- Confirmarlo con VPC Flow Logs
- Creación y auditoría por CLI
- 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/0en 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:
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-xpermite entrada desdesg-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 | Sí |
| Reglas de entrada por grupo | 60 | Sí, hasta 1.000 |
| Reglas de salida por grupo | 60 | Sí |
| Grupos de seguridad por ENI | 5 | Sí, hasta 16 |
| Reglas × grupos por ENI | 1.000 | Sí |
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 | Sí, 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:
- 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/0a las once de la noche. - No sirve para el teletrabajo. Marta desde casa tiene otra IP.
- No queda registro de quién entró. SSH con clave compartida no identifica a la persona.
- 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 | Sí | 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 | Sí, 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/32El 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 --ingressDos 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 tableDesglose 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 un0.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:
- ¿Hay camino y está permitido el paquete? → rutas (03-01), grupo de seguridad, NACL, endpoint de VPC.
- ¿Tiene permiso esa identidad para hacer
s3:GetObjectsobre ese objeto? → políticas de IAM, el rolrol-mercadofresco-tienda, y las políticas de bucket. Eso es materia de la lección 04-01, y unAccessDeniedde 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
- ¿Qué es AWS?
- Configuración de tu cuenta de AWS
- Infraestructura global de AWS
- Consola de administración de AWS
- AWS CLI y SDKs
Módulo 2: Servicios principales de AWS
Módulo 3: Redes y entrega de contenido
- Amazon VPC
- Grupos de seguridad y listas de control de acceso
- Elastic Load Balancing
- Amazon CloudFront
- Route 53
Módulo 4: Seguridad e identidad
- AWS Identity and Access Management (IAM)
- AWS Key Management Service (KMS)
- Secrets Manager y Parameter Store
- AWS Shield
- AWS WAF
Módulo 5: Monitorización y gestión
- Amazon CloudWatch
- AWS X-Ray y trazabilidad distribuida
- AWS CloudTrail
- AWS Config
- AWS Trusted Advisor
Módulo 6: Bases de datos
- Cómo elegir la base de datos adecuada
- Amazon DynamoDB
- Amazon Aurora
- Amazon Redshift
- Amazon ElastiCache
Módulo 7: Integración de aplicaciones
- Amazon SQS
- Amazon SNS
- Amazon EventBridge
- AWS Step Functions
- Patrones de integración: idempotencia, reintentos y colas de mensajes fallidos
