1Por qué importa
La superficie de ataque de red de un sistema no es lo que dice el diagrama de arquitectura. Es la lista de sockets en estado de escucha, cruzada con quién puede llegar a cada uno. Esa lista se obtiene con un comando, cambia con cada despliegue y casi nunca coincide con lo que el equipo cree.
Los tres desajustes que más veces he visto son siempre los mismos. Un servicio de depuración que alguien levantó para una tarde y quedó escuchando en todas las interfaces. Una base de datos con el puerto publicado “para poder mirarla desde el portátil” que nadie retiró. Y un contenedor que expone un puerto sin que su servicio esté pensado para recibir tráfico de nadie. Ninguno de los tres aparece en un diagrama; los tres aparecen en la tabla de sockets.
Hay un motivo más profundo para empezar la fase aquí. Todo lo que viene después —DNS, HTTP, TLS, firewalls— son capas construidas encima de esto. Si no tienes claro qué es una conexión, qué la establece y qué la corta, las lecciones siguientes se convierten en recetas. Con el modelo claro, se convierten en consecuencias.
El entregable de esta lección es un inventario: una tabla con protocolo, dirección de escucha, puerto, proceso y desde dónde es alcanzable. Esa última columna es la que convierte un listado técnico en una decisión de seguridad.
2Conceptos
- Capa de enlace: mueve tramas entre equipos del mismo segmento, por dirección MAC.
- Capa de red (IP): mueve paquetes entre redes distintas; aquí vive el encaminamiento.
- Capa de transporte: TCP y UDP; aquí aparecen los puertos y, en TCP, el estado de la conexión.
- Capa de aplicación: HTTP, DNS, Postgres; el contenido que a la red le da igual.
- Puerto: un número de 16 bits que multiplexa varias conversaciones sobre una misma dirección.
- Socket: la tupla completa (protocolo, dirección local, puerto local, dirección remota, puerto remoto).
- Escuchar: tener un socket en estado
LISTENesperando conexiones entrantes. - NAT: traducir direcciones y puertos al cruzar una frontera; el motivo de que “abierto” dependa de dónde preguntes.
3Explicación profunda
El modelo TCP/IP tiene cuatro capas y la utilidad de recordarlas no es académica: cada control de seguridad de red actúa en una capa concreta y es ciego a las demás. Un filtro que decide por dirección y puerto no sabe si dentro va HTTP o Postgres. Un proxy que entiende rutas HTTP no ve el encaminamiento. Cuando alguien dice “el firewall lo cubre”, la primera pregunta útil es en qué capa mira.
En la capa de transporte está la decisión que más consecuencias tiene.
TCP establece una conexión con un intercambio de tres pasos: el cliente
manda SYN, el servidor responde SYN-ACK y el cliente
confirma con ACK. A partir de ahí hay una conversación con números
de secuencia, acuse de recibo y retransmisión. UDP no tiene nada de eso:
manda un datagrama y termina su trabajo.
Esa diferencia tiene tres consecuencias de seguridad que conviene tener
memorizadas. La primera: en TCP, quien recibe sabe con quién habla,
porque completar el intercambio de tres pasos exige recibir de vuelta el número
de secuencia del servidor. En UDP no, y por eso la falsificación de origen es
trivial y por eso los ataques de amplificación viven casi siempre ahí. La
segunda: un filtro stateful puede seguir una conexión TCP y permitir
sus respuestas sin abrir un rango entero; con UDP tiene que aproximar con
tiempos de espera. La tercera: los escaneos se comportan distinto. Un puerto TCP
cerrado contesta RST de inmediato, así que se distingue de uno
filtrado, que no contesta nada. Un puerto UDP cerrado responde con un mensaje de
control, y muchos sistemas limitan la tasa de esos mensajes, con lo que un
escaneo UDP es lento y ambiguo.
El socket es el otro concepto que hay que tener sin nieblas. No es el
puerto: es la tupla de cinco elementos. Un servidor con un solo socket en
LISTEN sobre el puerto 443 mantiene simultáneamente miles de
sockets establecidos, uno por cliente, todos con el mismo puerto local. Cuando
miras una tabla de sockets tienes que separar mentalmente las dos poblaciones:
las líneas LISTEN son tu superficie de ataque; las líneas
ESTAB son tu tráfico.
Queda la parte que más confusión genera: la dirección de escucha. Un
proceso puede escuchar en la dirección de loopback, en una interfaz concreta o
en todas. Escuchar en loopback significa que solo los procesos de esa misma
pila de red llegan; es la diferencia entre un servicio de administración
razonable y un incidente. Escuchar en todas las interfaces significa todas las
de esa pila de red, que en un contenedor no son las de tu máquina. Por
eso publicar un puerto es un acto explícito y separado: sin ports
en el compose, el proceso escucha y no lo alcanza nadie de fuera.
NAT y el encaminamiento completan el cuadro. Cuando publicas un puerto, Docker escribe una regla de traducción: lo que llega a una dirección y puerto del host se reescribe hacia la dirección y el puerto del contenedor. La consecuencia práctica es que abierto es una propiedad relativa al punto desde el que preguntas, y por eso el inventario tiene que hacerse desde los dos lados. Es también la razón de que NAT no sea una medida de seguridad: no filtra nada, solo cambia direcciones. Que durante veinte años haya funcionado como cortafuegos accidental no lo convierte en uno.
4Ejemplo sencillo
# Un edificio de oficinas direccion IP la calle y el numero del edificio puerto el numero de despacho dentro del edificio socket "despacho 443 del edificio X hablando con el despacho 51844 del edificio Y" # Y los dos transportes TCP llamas al telefono: alguien descuelga, dice "si?", y sabes que te oye. Si se corta, los dos os enterais. UDP metes una carta por debajo de la puerta y te vas. Puede que llegue, puede que no, y puede que llegue despues de la siguiente. # Tres estados que la gente confunde ESCUCHA hay alguien en el despacho esperando visitas ALCANZABLE ademas existe un pasillo desde donde tu estas PUBLICADO ademas hay un cartel en la puerta de la calle
5Ejemplo técnico
Así se lee una tabla de sockets de verdad. Esta salida es del contenedor
app del laboratorio de esta lección, y cada columna dice algo que
importa:
Dos líneas para un solo servicio, porque nginx abre un socket para cada familia de direcciones. La dirección local es la parte que decide la exposición: aquí es “todas las interfaces del contenedor”. Y la columna del proceso es la que convierte el hallazgo en una tarea asignable: sin ella tienes un puerto abierto y ningún responsable.
| Comando | Qué responde | Qué NO responde |
|---|---|---|
ss -ltnp | Qué procesos escuchan en esta pila de red y en qué dirección | Quién puede llegar hasta ellos |
ss -tnp | Conexiones establecidas: con quién habla la máquina ahora | Qué podría escuchar y no lo hace |
docker compose ps | Qué puertos están publicados hacia el host | Qué escucha dentro sin publicar |
nc -z destino puerto | Si desde aquí se alcanza aquel socket | Si se alcanza desde otro sitio |
ip route | Por dónde saldría un paquete hacia cada red | Si algo lo va a filtrar por el camino |
La tabla resume el método: ningún comando responde la pregunta completa. El inventario correcto se hace desde dentro del sistema y desde fuera, y las discrepancias entre las dos vistas son precisamente los hallazgos.
6Diagrama
Ver el mismo diagrama como código Mermaid (editable)
flowchart LR
A[aplicacion] --> PA[proxy: ruta y cookie]
T[transporte] --> PT[firewall L4: puerto y estado]
R[red] --> PR[segmentacion: hay ruta o no]
E[enlace] --> PE[ARP: sin autenticacion]
A --- T --- R --- E
7Código
Un inventario de exposición cabe en un script y en una tabla. Este recorre las dos vistas —desde dentro y desde fuera— y deja las discrepancias a la vista, que es lo único que hay que revisar a mano:
#!/bin/sh # inventario-sockets.sh — que escucha, y desde donde se alcanza. # Se ejecuta contra el laboratorio propio. Nada de esto apunta fuera. set -eu SERVICIOS="entrada app db cliente" echo "== vista de dentro: sockets en escucha ==" for s in $SERVICIOS; do echo "--- $s" # -l solo LISTEN, -t TCP, -u UDP, -n sin resolver, -p con proceso docker compose exec -T "$s" ss -ltunp 2>/dev/null || echo "sin ss en la imagen" done echo echo "== vista de fuera: lo que el host publica ==" docker compose ps --format "table {{.Service}}\t{{.Ports}}" echo echo "== alcanzabilidad real desde el puesto de trabajo ==" for destino in "app 80" "db 5432" "app 8080"; do set -- $destino if docker compose exec -T cliente nc -z -w 2 "$1" "$2" 2>/dev/null; then echo "ALCANZABLE $1:$2" else echo "no $1:$2" fi done
Lo valioso del script no es ninguno de los tres bloques por separado: es el contraste. Un socket que aparece en la vista de dentro y no en la de fuera está correctamente contenido. Uno que aparece en las dos es superficie de ataque declarada. Y uno que resulta alcanzable desde un contenedor que no debería hablar con él es un hallazgo de segmentación, que es justo lo que la última lección de esta fase va a arreglar.
8Qué puede salir mal
Publicar sin prefijo de interfaz. Escribir 8080:80 en
lugar de 127.0.0.1:8080:80 publica el puerto en todas las
interfaces del host. En una cafetería, tu laboratorio deliberadamente
vulnerable acaba de ser el servicio más interesante de la red.
Confundir “no lo uso” con “no escucha”. Un servicio de depuración
arrancado por una variable de entorno olvidada sigue en LISTEN
aunque nadie lo llame. El inventario no pregunta por intenciones.
Creer que NAT protege. NAT traduce, no filtra. En cuanto existe una regla de publicación, o una conexión saliente que abre el camino de vuelta, el servicio está expuesto. Toda la sensación de seguridad de las redes domésticas viene de un efecto secundario, no de un control.
Inventariar solo TCP. ss -ltn omite UDP por completo.
Un resolutor mal configurado o un servicio de métricas en UDP no aparecen,
y son exactamente el tipo de cosa que se usa para amplificar.
9Cómo lo abusaría un atacante
Lo primero que hace alguien que llega a una red no es explotar nada: es
construir el mismo inventario que acabas de aprender a construir, pero
desde fuera. Barre puertos, anota qué responde y clasifica cada respuesta.
Un RST le dice “aquí hay una máquina y ese puerto está
cerrado”, que ya es información: confirma el equipo. El silencio le dice
“hay un filtro”, que también lo es.
La segunda cosa que busca es la asimetría entre vistas, porque es donde vive el descuido. Un servicio que no está publicado hacia internet pero sí es alcanzable desde cualquier contenedor del mismo segmento es el camino estándar del movimiento lateral. No hace falta romper nada: basta con haber llegado a un sitio cualquiera de esa red.
Y la tercera es el orden de los paquetes. Un escaneo que completa el
intercambio de tres pasos deja una conexión aceptada y, casi siempre, una
línea en el log de la aplicación. Uno que manda solo el SYN y
abandona no llega a la aplicación y, en muchos sistemas, no deja rastro
salvo que alguien esté mirando la capa de transporte. La lección defensiva
está clara: si tu única fuente de detección son los logs de aplicación,
eres ciego a la mitad del reconocimiento.
10Cómo defenderlo
- Escucha en loopback por defecto. Un servicio se ata a todas las interfaces cuando alguien escribe por qué en el ticket, no antes.
- Publica con interfaz explícita.
127.0.0.1:puerto:puertoen todo lo que no sea deliberadamente público. Es una línea y evita una clase entera de incidentes. - Inventaría las dos vistas y guarda el resultado en el repositorio. Un cambio en esa tabla es un cambio de superficie de ataque y merece revisión.
- Incluye UDP en cada inventario. Cuesta una letra en el comando y cubre la mitad que todo el mundo olvida.
- Separa segmentos por función. Que el puesto de trabajo alcance la base de datos no es una comodidad, es el camino del movimiento lateral.
- Registra a nivel de conexión, no solo de aplicación, si quieres ver el reconocimiento antes que la explotación.
11Laboratorio
Levanta el laboratorio. La primera vez construye la imagen de herramientas:
cd cursos/seguridad/labs/lab-net-captura
docker compose up -d --build
docker compose ps
Ahora las dos vistas. Primero desde dentro de la aplicación, después desde el host, y fíjate en que no dicen lo mismo:
Comprueba ahora la diferencia entre cerrado y filtrado, que es la que
un atacante usa para dibujar el mapa. El puerto 5432 de db
está abierto; el 3306 no existe en ninguna parte del laboratorio:
docker compose exec cliente nc -z -v -w 2 db 5432
docker compose exec cliente nc -z -v -w 2 db 3306
docker compose exec cliente ss -tnp
Genera tráfico contra el borde y mira la tabla de conexiones establecidas mientras ocurre. Cuando termines, destruye el laboratorio:
curl -s http://localhost:8091/ > /dev/null
docker compose logs --tail 5 entrada
docker compose down -v
12Ejercicios
Clasifica cinco líneas de socket
Para cada una, di si es superficie de ataque hacia el host, superficie
interna o tráfico: (1) LISTEN 0.0.0.0:80 en un contenedor sin
ports; (2) LISTEN 127.0.0.1:5432 en tu portátil;
(3) ESTAB hacia el puerto 443 de otra máquina;
(4) LISTEN 0.0.0.0:9229 en un contenedor con
9229:9229 publicado; (5) LISTEN [::]:53 en UDP.
LISTEN de las ESTAB: solo las primeras son superficie.LISTEN, pregunta desde qué pila de red se llega a esa dirección.
(1) superficie interna: escucha en todas las interfaces del contenedor,
pero sin publicación no llega el host. (2) superficie muy reducida: solo
procesos locales. (3) tráfico, no superficie. (4) superficie hacia el
host y hacia la red local, y además es un puerto de depuración, que
suele significar ejecución de código. (5) superficie interna en UDP, y
es la línea que un inventario con ss -ltn no habría visto.
Clave: interna · local · tráfico · externa y grave · interna en UDP. El ejercicio está bien resuelto si en el (4) mencionas las dos cosas.
Publica la base de datos y mide el daño
En el lab-net-captura, añade a db una publicación
de puerto en la interfaz de loopback. Comprueba que ahora se alcanza desde
el host. Después cámbiala por la forma sin interfaz y explica, con un
comando que lo demuestre, qué cambió. Revierte las dos cosas.
db del compose no tiene bloque ports. Añádelo y vuelve a levantar con up -d.ss -ltn muestra la dirección en la que Docker ha atado el puerto publicado. Compárala antes y después.# forma correcta: solo tu maquina db: ports: - "127.0.0.1:5432:5432" # forma peligrosa: toda la red local (NO dejarlo asi) db: ports: - "5432:5432"
Con la primera forma, la tabla de sockets del host muestra el puerto atado a la dirección de loopback y nadie más de la red llega. Con la segunda aparece atado a todas las interfaces: cualquier equipo del mismo wifi puede intentar autenticarse contra tu Postgres, que además tiene la contraseña escrita en claro en el compose.
Clave: la demostración vale más que la explicación. Se pide un ss -ltn del host antes y después, y que revirtieras ambos cambios.
Reconstruye el intercambio de tres pasos
Captura una conexión completa contra app desde el contenedor
sniffer e identifica, en la salida de tcpdump, los tres
paquetes del establecimiento y los que cierran la conexión. Después
responde: ¿por qué a veces ves cuatro paquetes de cierre y a veces tres?
Necesitarás mirar documentación fuera de esta lección.
[S], [S.] y [.].FIN y RST, y no todos los clientes cierran igual: HTTP con conexiones persistentes cambia el final.docker compose exec sniffer tcpdump -i any -c 20 tcp port 80 y lanza el curl desde otra terminal.
El establecimiento son siempre tres: SYN,
SYN-ACK, ACK. El cierre limpio son cuatro,
porque cada lado cierra su mitad por separado: FIN y
ACK en un sentido, y lo mismo en el otro. Se ven tres
cuando el ACK del primero viaja junto al FIN
del segundo, y se ve un cierre abrupto con RST cuando
alguien corta sin negociar.
La consecuencia de seguridad es que el número de paquetes no es un detalle: un filtro con estado tiene que decidir cuánto tiempo recuerda una conexión medio cerrada, y ese temporizador es un recurso finito que se puede agotar a propósito.
Clave: tres para abrir, tres o cuatro para cerrar según se combinen las banderas, y uno solo si hay RST. La parte evaluable es que expliques por qué el cierre es bidireccional.
Red Team · dibuja el mapa desde dentro
Ponte en el papel de alguien que acaba de conseguir ejecución en el
contenedor cliente y no sabe nada más. Solo dentro del
lab-net-captura: enumera qué otros equipos existen en su red,
qué puertos tienen abiertos y qué servicio parece correr en cada uno.
Escribe el mapa resultante y señala cuál sería tu siguiente objetivo y por
qué.
nc -z en un bucle; no hace falta ninguna herramienta especializada.docker compose exec cliente sh -c 'for p in 22 53 80 443 5432 8080; do nc -z -w 1 db $p && echo abierto $p; done' y repítelo por cada nombre.
El mapa que sale es: entrada y app con el 80
abierto, db con el 5432, y nada más. El siguiente objetivo
razonable es db, y no por ser una base de datos sino por
dos motivos concretos: es el único servicio con estado persistente, y
su credencial suele estar escrita en el entorno de otro contenedor al
que quizá ya tienes acceso.
Fíjate en lo que no hizo falta: ninguna vulnerabilidad. Todo el reconocimiento se hace con permisos legítimos de red, y por eso la defensa contra esta fase no es parchear, es segmentar.
Clave: el entregable es el mapa más la justificación del siguiente salto. Guárdalo: en f02-firewall vas a cortar exactamente estos caminos.
Blue Team · qué dejó el barrido en los logs
Repite el barrido del ejercicio anterior con el laboratorio levantado y
después busca rastro en docker compose logs. Responde: ¿cuáles
de tus intentos dejaron una línea y cuáles no? ¿Qué fuente de datos
necesitarías para ver los que faltan?
sniffer mientras haces el barrido.
Un nc -z contra nginx abre y cierra sin enviar una
petición, así que el log de acceso no registra nada; a lo sumo aparece
una línea en el log de error. Postgres es más ruidoso y suele dejar
constancia de una conexión incompleta. Los intentos contra puertos
donde no escucha nadie no dejan absolutamente nada, porque no hay
proceso que pueda escribirlo.
La fuente que falta es la capa de transporte: registro a nivel de conexión, sea del filtro, del nodo o de una captura. Sin ella, todo un barrido de reconocimiento es invisible para tu pila de observabilidad.
Clave: la respuesta correcta es “casi nada, y esa es la noticia”. El entregable es la lista de fuentes que harían visible el reconocimiento.
Architecture Challenge · el inventario de exposición como control
Diseña cómo integrarías el inventario de sockets en un sistema real para que detecte cambios de superficie de ataque. Define: dónde se ejecuta, con qué frecuencia, contra qué línea base se compara, qué diferencia bloquea un despliegue y cuál solo avisa, y cómo evitas que el ruido lo vuelva inútil en tres semanas.
Una propuesta razonable: la línea base es un archivo versionado con la tabla esperada de sockets por servicio. El inventario se ejecuta en el pipeline contra el entorno recién levantado y también de forma periódica en producción. Bloquea cuando aparece un socket que no está en la línea base, o cuando uno pasa de loopback a todas las interfaces, porque ambas cosas son cambios de exposición. Avisa cuando desaparece un socket esperado, que suele ser un servicio caído y no un problema de seguridad.
Contra el ruido, dos decisiones de diseño: normalizar puertos efímeros y conexiones establecidas fuera del inventario, y exigir que cualquier entrada nueva en la línea base lleve un campo con el motivo. Ese campo es lo que hace que la revisión sea una conversación y no un sello.
Clave: no hay una única respuesta. Se evalúa que distingas bloqueo de aviso, que la línea base viva junto al código y que hayas pensado en el ruido antes de que aparezca.
13Preguntas de comprensión
¿Qué te dice ss -ltnp dentro de un contenedor que no te dice docker compose ps?
El compose describe la intención declarada; la tabla de sockets describe el estado real. Las discrepancias entre ambas son el hallazgo.
Intentas conectar a un puerto TCP y recibes un RST inmediato. ¿Cómo está ese puerto?
El RST confirma que la máquina existe. El silencio es lo que indica un filtro, y por eso descartar revela menos que rechazar.
¿Por qué el curso escribe siempre 127.0.0.1:8091:80 y nunca 8091:80?
La forma corta expone el laboratorio a toda la red local. Es una diferencia de trece caracteres entre practicar y ser un problema para terceros.
Un escaneo que completa el intercambio de tres pasos deja en el objetivo…
Por eso existen los escaneos que no completan la conexión: dejan menos rastro justo en la fuente que casi todo el mundo monitoriza.
14Reto adicional
Inventaría tu propia máquina de desarrollo
Ejecuta el inventario de sockets contra tu equipo de trabajo, no contra el laboratorio. Lista todo lo que escucha en direcciones que no son de loopback, identifica el proceso responsable de cada línea y decide, para cada una, si debería seguir ahí. No cambies nada todavía: escribe la tabla y la decisión.
No hay una respuesta única, pero sí un patrón que se repite: la mayoría de las máquinas de desarrollo tienen entre tres y diez servicios escuchando en todas las interfaces sin que nadie lo haya decidido. Los más comunes son servidores de desarrollo que se atan a todas las interfaces por comodidad, demonios de compartición de archivos y puertos de depuración de intérpretes.
Lo interesante del ejercicio no es la lista, es la pregunta que la acompaña: cuando trabajas desde una red que no controlas, esa lista es exactamente lo que ofreces. Un servidor de desarrollo con recarga automática suele leer y escribir archivos de tu proyecto sin autenticación de ningún tipo.
Clave: el entregable es la tabla con la columna de decisión. Si en alguna fila no sabes qué proceso es, esa fila es el hallazgo.
15Resumen
- Cada control de red actúa en una capa y es ciego a las de arriba. “El firewall lo cubre” no significa nada sin decir en qué capa mira.
- TCP mantiene estado y confirma el origen; UDP no. De ahí salen la falsificación de origen, la amplificación y la dificultad de filtrar UDP con precisión.
- Un socket es la tupla completa, no el puerto. Las líneas
LISTENson superficie de ataque; lasESTABson tráfico. - Escuchar, ser alcanzable y estar publicado son tres cosas distintas, y el inventario hay que hacerlo desde dentro y desde fuera.
- Un puerto cerrado responde
RST; uno filtrado calla. La respuesta que das es información que regalas. - NAT traduce, no filtra. La sensación de protección que da es un efecto secundario, no un control.
ports. ¿Está expuesto al host?16Mis notas
Estas notas se guardan solo en este navegador. No se envían a ningún servidor. Expórtalas desde el panel de progreso si cambias de máquina.
17Recursos
Los comentarios se cargan solo si los pides, desde GitHub Discussions. No se carga nada de terceros mientras no pulses.