En la lección anterior pusiste cerradura a la puerta principal. Pero srv-tramontana sigue teniendo todas las ventanas abiertas: cualquiera que alcance la máquina puede hablar con el 8080 de la aplicación, y si mañana PostgreSQL escuchara en 0.0.0.0 en lugar de en localhost, también con el 5432. El bot de 203.0.113.44 sigue llamando 47 veces por madrugada y nadie se lo impide.

Un cortafuegos responde a una pregunta anterior a la de la autenticación: ¿quién tiene siquiera derecho a hablar con este servidor, y por qué puerto? Al terminar esta lección tendrás una política escrita y aplicada —solo SSH limitado y HTTPS desde fuera, PostgreSQL solo desde la red interna, el 8080 cerrado al exterior—, entenderás qué hay debajo de ufw, sabrás escribir un ruleset de nftables desde cero y tendrás a fail2ban bloqueando automáticamente a quien insista. Y todo ello sin perder la sesión SSH, porque la regla de oro no cambia: nunca cierres la puerta por la que estás entrando.

Contenido

  1. Qué es y qué no es un cortafuegos
  2. El modelo de Linux: netfilter y sus frontales
  3. Tablas, cadenas, hooks y estado de conexión
  4. ufw en la práctica
  5. Perfiles de aplicación
  6. Cuando ufw no llega: nftables directo
  7. Equivalencias iptables → nftables
  8. Reglas de salida y por qué casi nadie las pone
  9. sysctl de red con impacto en seguridad
  10. fail2ban: del registro al bloqueo automático
  11. Verificar desde fuera con nmap
  12. Caso Tramontana: la política y el informe a Marta

  1. Qué es y qué no es un cortafuegos

Un cortafuegos decide qué paquetes entran, salen o atraviesan una máquina, según reglas que tú escribes. Eso es todo lo que hace, y conviene tenerlo muy claro:

Un firewall sí Un firewall no
Reduce la superficie expuesta a la red Arregla un servicio mal configurado
Limita quién puede intentar autenticarse Impide un ataque por el puerto que sí dejas abierto
Frena escaneos y ruido automatizado Detecta que ya te han entrado (eso es 06-04)
Contiene el daño lateral tras un compromiso Sustituye a las actualizaciones de seguridad

La defensa en profundidad consiste precisamente en no depender de una sola capa: si un atacante supera el firewall, se encuentra con SSH sin contraseñas; si supera SSH, se encuentra con svc-tramontana sin shell y con ProtectSystem=strict; si llega al disco, se encuentra con los secretos cifrados del 06-05. Ninguna capa es suficiente sola, y esa es la idea.

  1. El modelo de Linux: netfilter y sus frontales

En el kernel hay un único motor de filtrado, netfilter. Todo lo demás son formas de escribirle reglas:

Capa Qué es Estado en Ubuntu 24.04
netfilter El motor, dentro del kernel Lo que realmente filtra
nftables El lenguaje y la herramienta nft actuales La opción nativa: un solo comando para IPv4, IPv6, ARP y bridge
iptables El interfaz histórico Presente como iptables-nft: traduce a nftables por debajo
ufw Uncomplicated Firewall, frontal de Ubuntu Instalado por defecto, escribe reglas de nftables
firewalld Frontal con zonas, típico de RHEL Disponible, poco habitual aquí

La consecuencia práctica: en Ubuntu 24.04, cuando escribes ufw allow 22/tcp, ufw genera reglas que acaban en el mismo motor nftables que verías con nft list ruleset. No son sistemas rivales: son dos niveles de abstracción sobre lo mismo.

Y la advertencia que ahorra tardes enteras: usa uno u otro, no los dos a la vez. Si activas ufw y además habilitas el servicio nftables con tu propio /etc/nftables.conf, tendrás dos conjuntos de reglas compitiendo, un flush ruleset que borra las de ufw en cada arranque y un comportamiento imposible de razonar.

  1. Tablas, cadenas, hooks y estado de conexión

Cuatro conceptos y ya puedes leer cualquier ruleset:

  • Tabla: el contenedor. En nftables se declara con una familia: ip (IPv4), ip6, inet (las dos a la vez, lo que querrás casi siempre), arp, bridge, netdev.
  • Cadena: un conjunto ordenado de reglas colgado de un hook del kernel. Tres importan: input (paquetes destinados a esta máquina, donde escribes casi todo), output (paquetes generados por ella, el egress del apartado 8) y forward (paquetes que la atraviesan, solo si es router o VPN → 08-04).
  • Política por defecto: qué pasa con un paquete que no encaja en ninguna regla. Solo hay dos filosofías, y una es la correcta: policy drop (lista blanca: se prohíbe todo y se permite lo necesario) frente a policy accept (lista negra: se permite todo y se prohíbe lo malo conocido). La lista negra es imposible de mantener, porque exige conocer de antemano todo lo que puede ir mal.
  • Estado de conexión: netfilter recuerda las conexiones en curso (connection tracking). Eso permite escribir ct state established,related accept y despreocuparte del tráfico de vuelta.

Ese último punto es el que lo cambia todo. Sin filtrado con estado tendrías que abrir a mano los puertos altos para las respuestas, lo que equivale a no filtrar nada. Con estado la lógica es limpísima: new es el primer paquete de una conexión y es ahí donde se decide; established pertenece a una conexión ya aceptada y se acepta sin mirar; related es una conexión secundaria de otra (errores ICMP, datos de FTP) y también se acepta; e invalid no encaja en ninguna conexión conocida, así que se descarta.

  1. ufw en la práctica

ufw es lo que usarás el 95 % de las veces. Empieza siempre midiendo:

$ sudo ufw status verbose
Status: inactive

El orden correcto de operaciones, y no hay ninguna otra forma de hacerlo bien en una máquina remota:

# 1. Políticas por defecto: denegar entrante, permitir saliente
$ sudo ufw default deny incoming
$ sudo ufw default allow outgoing
$ sudo ufw default deny routed
# 2. PRIMERO permitir SSH. Sin esto, el paso 4 te deja fuera.
$ sudo ufw limit 22/tcp comment 'SSH con limitacion de intentos'
# 3. El resto de servicios
$ sudo ufw allow 443/tcp comment 'HTTPS publico'
$ sudo ufw allow from 10.0.2.0/24 to any port 5432 proto tcp comment 'PostgreSQL red interna'
# 4. AHORA sí, habilitar
$ sudo ufw enable
Command may disrupt existing ssh connections. Proceed with operation (y|n)? y
Firewall is active and enabled on system startup

Ese aviso del paso 4 no es decorativo: si ejecutas ufw enable con la política deny incoming y sin haber permitido SSH, la sesión se corta en el acto y solo recuperas la máquina por la consola de la VM. Ten esa consola abierta antes de empezar.

$ sudo ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), deny (routed)
New profiles: skip

To                         Action      From
--                         ------      ----
22/tcp                     LIMIT IN    Anywhere                   # SSH con limitacion
443/tcp                    ALLOW IN    Anywhere                   # HTTPS publico
5432/tcp                   ALLOW IN    10.0.2.0/24                # PostgreSQL red interna
22/tcp (v6)                LIMIT IN    Anywhere (v6)

Cuatro detalles que importan de esta salida:

  • ufw crea automáticamente la regla IPv6 equivalente. Es exactamente el motivo por el que no se desactiva IPv6 «por si acaso»: aquí está cubierto.
  • LIMIT no es ALLOW: ufw limit bloquea una IP que abre 6 o más conexiones en 30 segundos. Es una defensa barata contra fuerza bruta, aunque fail2ban (apartado 10) es mucho más fino.
  • El comment es documentación viva, y deny (routed) deja claro que esta máquina no reenvía nada, coherente con el ip_forward = 0 del 06-01.

deny frente a reject, que es una diferencia real y no un matiz:

Acción Qué hace el kernel Lo que ve quien llama Cuándo usarla
deny (DROP) Descarta en silencio Timeout: no sabe si la máquina existe Cara a Internet: cuesta tiempo al escáner
reject Responde ICMP port unreachable o TCP RST «Cerrado» inmediato Red interna: evita esperas de 30 s a tus aplicaciones

Gestionar reglas existentes exige verlas numeradas, porque el orden importa: gana la primera que encaja.

$ sudo ufw status numbered
     To                         Action      From
     --                         ------      ----
[ 1] 22/tcp                     LIMIT IN    Anywhere
[ 2] 443/tcp                    ALLOW IN    Anywhere
[ 3] 5432/tcp                   ALLOW IN    10.0.2.0/24
$ sudo ufw delete 3
$ sudo ufw insert 1 deny from 203.0.113.44 comment 'bloqueo manual bot'

Ojo: al borrar la regla 3, las siguientes se renumeran. Borra siempre de mayor a menor, o mejor, borra por especificación (sudo ufw delete allow 443/tcp), que es idempotente y no depende de números.

  1. Perfiles de aplicación

ufw trae perfiles con nombre en /etc/ufw/applications.d/, que traducen «OpenSSH» a «puerto 22/tcp»:

$ sudo ufw app list
Available applications:
  Nginx Full
  OpenSSH
$ sudo ufw app info OpenSSH
Profile: OpenSSH — Ports: 22/tcp
$ sudo ufw allow OpenSSH

Y puedes escribir el tuyo, que es la forma limpia de documentar los puertos de tu aplicación:

[Tramontana]
title=Tramontana Reservas
description=Aplicacion web de reservas (proxy inverso en 08-01)
ports=443/tcp

Guárdalo en /etc/ufw/applications.d/tramontana y recárgalo con sudo ufw app update Tramontana: si mañana cambia el puerto, se cambia en un solo sitio.

  1. Cuando ufw no llega: nftables directo

ufw se queda corto cuando necesitas limitación de tasa fina, conjuntos de direcciones (set), marcado de paquetes, NAT elaborado o simplemente leer exactamente lo que hay. Entonces se escribe nftables a mano, en /etc/nftables.conf:

#!/usr/sbin/nft -f
flush ruleset

table inet filter {
    set redes_internas {
        type ipv4_addr ; flags interval ; elements = { 10.0.2.0/24 }
    }

    chain input {
        type filter hook input priority 0; policy drop;
        # Tráfico de vuelta y basura: lo primero, por eficiencia
        ct state established,related accept
        ct state invalid drop
        # Loopback: sin esto se rompen medio sistema y los túneles SSH
        iif lo accept
        # ICMP imprescindible (no bloquees ICMP entero: rompe la MTU)
        ip protocol icmp icmp type { echo-request, destination-unreachable, time-exceeded } accept
        ip6 nexthdr ipv6-icmp accept
        # SSH con limitación de intentos nuevos, y HTTPS público
        tcp dport 22 ct state new limit rate 6/minute burst 6 packets accept
        tcp dport 443 accept
        # PostgreSQL solo desde la red interna
        ip saddr @redes_internas tcp dport 5432 accept
        # Lo demás cae por la política, dejando rastro acotado
        limit rate 5/minute log prefix "nft-drop-in: " level info
        counter
    }

    chain forward {
        type filter hook forward priority 0; policy drop;
    }

    chain output {
        type filter hook output priority 0; policy accept;
    }
}

Comentarios sobre el diseño, que es lo que se aprende:

  • table inet cubre IPv4 e IPv6 con las mismas reglas. Con iptables necesitarías duplicarlo todo en ip6tables, y ahí es donde la gente se deja agujeros.
  • policy drop en input y forward, accept en output: lista blanca donde importa.
  • El orden es deliberado: lo más frecuente arriba (established), lo excepcional abajo. Cada paquete recorre la cadena hasta encontrar su regla.
  • No bloquees todo ICMP. destination-unreachable transporta el descubrimiento de MTU: sin él tendrás conexiones que se cuelgan al transferir ficheros grandes y te volverás loco buscando la causa.
  • El log final lleva su propio limit rate, porque un escaneo puede generar miles de líneas por segundo y llenarte el disco. Registrar lo que se descarta es lo que hace diagnosticable el firewall.
$ sudo nft -c -f /etc/nftables.conf && echo "sintaxis correcta"
sintaxis correcta
$ sudo systemctl enable --now nftables
$ sudo nft list ruleset | head -2
table inet filter {
        set redes_internas {

nft -c comprueba sin aplicar: es el netplan generate de los cortafuegos, y se usa siempre. La persistencia la da el servicio nftables, que ejecuta /etc/nftables.conf en cada arranque; sin él, nft es tan volátil como ip addr add.

  1. Equivalencias iptables → nftables

Sigue habiendo montañas de documentación escrita en iptables. Esta tabla te permite leerla:

iptables nftables
-A INPUT -p tcp --dport 22 -j ACCEPT tcp dport 22 accept
-P INPUT DROP policy drop en la definición de la cadena
-m state --state ESTABLISHED,RELATED -j ACCEPT ct state established,related accept
-s 10.0.2.0/24 / -i lo -j ACCEPT ip saddr 10.0.2.0/24 / iif lo accept
-j REJECT --reject-with icmp-port-unreachable reject with icmpx type port-unreachable
iptables -L -n -v / iptables-save nft list ruleset
Reglas separadas en iptables/ip6tables Una sola en table inet

  1. Reglas de salida y por qué casi nadie las pone

Casi todo el mundo escribe output policy accept y se olvida. Es cómodo y es una oportunidad perdida: las reglas de egress no impiden que te entren, pero limitan muchísimo lo que un atacante puede hacer después. Sin salida libre, no puede descargar su segunda fase desde un servidor externo, no puede exfiltrar reservas.csv a un host cualquiera y no puede unirse a una botnet.

    chain output {
        type filter hook output priority 0; policy drop;
        ct state established,related accept
        oif lo accept
        udp dport { 53, 123 } accept       # DNS y NTP
        tcp dport { 53, 80, 443 } accept   # DNS/TCP y repositorios apt
        ip daddr 10.0.2.0/24 accept        # red interna
        limit rate 5/minute log prefix "nft-drop-out: "
    }

Por qué casi nadie las pone, con honestidad: rompen cosas de forma sutil y difícil de diagnosticar. Un apt update contra un espejo nuevo, un webhook, una actualización de certificados... todo falla con timeouts que nadie relaciona con el firewall. Si las adoptas, hazlo midiendo primero qué salidas usa de verdad la máquina (con el log en output y política accept durante una semana) y solo después cambiando la política a drop.

  1. sysctl de red con impacto en seguridad

Estos parámetros no son de rendimiento —eso es 07-03—, sino de comportamiento del stack de red frente a abusos:

$ sudo tee /etc/sysctl.d/60-seguridad-red.conf > /dev/null <<'CONF'
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.tcp_syncookies = 1
net.ipv4.icmp_echo_ignore_broadcasts = 1
CONF
$ sudo sysctl --system | grep -A8 60-seguridad
Parámetro Qué evita
rp_filter = 1 Paquetes con origen falsificado que no volverían por la misma interfaz (antispoofing)
accept_redirects = 0 / send_redirects = 0 Que un ICMP redirect ajeno reescriba tu tabla de rutas, y que tú informes de topología a terceros
accept_source_route = 0 Paquetes que dictan su propio camino, técnica clásica de evasión
tcp_syncookies = 1 Que una inundación SYN agote la cola de conexiones a medias
icmp_echo_ignore_broadcasts = 1 Ser amplificador de un ataque smurf
log_martians = 1 Registra paquetes imposibles: señal temprana de algo raro

Ojo con rp_filter = 1 (estricto): en una máquina con varias interfaces y rutas asimétricas descarta tráfico legítimo. En srv-tramontana, con una sola interfaz, es seguro; en un router o una máquina con VPN, usa 2 (modo laxo).

  1. fail2ban: del registro al bloqueo automático

fail2ban cierra el bucle: lee los logs, detecta patrones de abuso y le pide al firewall que bloquee la IP durante un tiempo. No sustituye al cortafuegos, lo pilota.

$ sudo apt install -y fail2ban
$ sudo tee /etc/fail2ban/jail.local > /dev/null <<'CONF'
[DEFAULT]
# NUNCA te bloquees a ti mismo: red interna y portátiles del equipo
ignoreip  = 127.0.0.1/8 ::1 10.0.2.0/24
bantime   = 1h
findtime  = 10m
maxretry  = 5
backend   = systemd
banaction = ufw

[sshd]
enabled   = true
port      = ssh
maxretry  = 3
bantime   = 24h
CONF
$ sudo systemctl enable --now fail2ban
$ sudo fail2ban-client status sshd
Status for the jail: sshd
|- Filter
|  |- Total failed:     47
|  `- Journal matches:  _SYSTEMD_UNIT=ssh.service + _COMM=sshd
`- Actions
   |- Currently banned: 1
   `- Banned IP list:   203.0.113.44

Se edita jail.local, nunca jail.conf: el segundo pertenece al paquete y la próxima actualización se lleva tus cambios por delante. Las tres cifras que gobiernan todo:

Parámetro Significado Valor elegido
maxretry / findtime Fallos tolerados y ventana en que se cuentan 3 en 10 minutos
bantime Duración del bloqueo 24 h (-1 sería permanente)
banaction Cómo bloquea ufw, para no crear un tercer conjunto de reglas
ignoreip Quién es inmune Tu red y tus portátiles

ignoreip es la protección contra el error más humillante de todos: teclear mal la contraseña de sudo tres veces y que tu propio servidor te bloquee. Si aun así ocurre:

$ sudo fail2ban-client set sshd unbanip 10.0.2.77
$ sudo fail2ban-client set sshd banip 203.0.113.44

Y la honestidad que toca: con PasswordAuthentication no del 06-02, fail2ban ya no te protege de que adivinen nada, porque no hay nada que adivinar. Lo que hace es reducir ruido, consumo y superficie: menos conexiones, menos logs, menos oportunidades ante un fallo futuro de sshd. Es una medida de higiene, no la que te salva.

  1. Verificar desde fuera con nmap

Un firewall no está verificado hasta que lo miras desde fuera. Desde portatil-alumno, contra tu propia VM:

$ nmap -Pn -p 22,443,5432,8080 10.0.2.15
Nmap scan report for srv-tramontana (10.0.2.15)
PORT     STATE    SERVICE
22/tcp   open     ssh
443/tcp  open     https
5432/tcp open     postgresql
8080/tcp filtered http-proxy

Cómo se lee: open responde, closed responde que no hay nadie, y filtered significa que algo descarta el paquete en silencio — justo el efecto de tu política deny. El 8080 aparece filtered: objetivo cumplido. El 5432 sale open porque el escaneo viene de 10.0.2.77, que está dentro de la red interna permitida; repetido desde fuera de ese rango saldría filtered.

Advertencia legal, sin matices: escanear puertos de sistemas ajenos sin autorización escrita es ilegal en España y en la práctica totalidad de las jurisdicciones, y puede constituir delito aunque no causes daño. En este curso todo escaneo se hace desde portatil-alumno contra srv-tramontana, ambas tuyas y dentro de tu laboratorio. Que una herramienta sea legal no hace legal cualquier uso: la diferencia entre auditoría y ataque es el permiso, y el permiso se documenta por escrito antes de teclear nada.

  1. Caso Tramontana: la política y el informe a Marta

La política queda así, y se decide antes de tocar ningún comando:

Servicio Puerto Desde dónde Motivo
SSH 22/tcp Cualquiera, con limit Administración; endurecido en 06-02
HTTPS 443/tcp Cualquiera Cara pública; el proxy llega en 08-01
PostgreSQL 5432/tcp Solo 10.0.2.0/24 Nunca desde Internet
Aplicación 8080/tcp Nadie Quedará detrás del proxy inverso (08-01)
Todo lo demás — Denegado Es una lista blanca

Diagnóstico cuando «el servicio no responde» y la causa eres tú: primero mira si el servicio escucha (ss -tulpn | grep 8080), después si el firewall lo descarta.

$ sudo journalctl -k --since "-15m" | grep 'UFW BLOCK' | tail -2
ago 18 19:22:41 srv-tramontana kernel: [UFW BLOCK] IN=enp0s3 SRC=203.0.113.44 DST=10.0.2.15 PROTO=TCP SPT=44210 DPT=8080 SYN
ago 18 19:23:02 srv-tramontana kernel: [UFW BLOCK] IN=enp0s3 SRC=198.51.100.9 DST=10.0.2.15 PROTO=TCP SPT=51882 DPT=23 SYN

Se lee de izquierda a derecha: entró por enp0s3, desde SRC, hacia DPT (puerto destino). La primera línea es el bot buscando la aplicación en el 8080; la segunda, alguien probando Telnet en 2026. Si quien aparece bloqueado es tu propio servicio legítimo, ya sabes qué regla falta.

El informe a Marta, con la estructura del curso —qué protege y qué no protege—:

Firewall aplicado en srv-tramontana el 18/08. Qué protege: solo son accesibles desde fuera SSH (con limitación de intentos) y HTTPS; la base de datos solo acepta conexiones de la red interna; el puerto 8080 de la aplicación ya no es alcanzable desde Internet; la IP 203.0.113.44, con 47 intentos de acceso, está bloqueada automáticamente 24 horas por fail2ban, y cualquier otra que lo intente lo estará también. Qué NO protege: no defiende de un fallo en la propia aplicación web, que sigue siendo accesible por el puerto que debe estar abierto; no impide nada a quien tenga credenciales válidas; no detecta que ya nos hayan entrado (eso se aborda en la revisión de detección); y no cifra el tráfico, que sigue viajando en claro hasta que instalemos el certificado. Pendiente: cifrado TLS y sacar la contraseña de la base de datos del fichero de configuración.

Advertencia de seguridad y compliance: en un entorno real, cualquier cambio en las reglas de cortafuegos de un sistema que trata datos personales debe planificarse con ventana de mantenimiento, quedar documentado y revisarlo el responsable de seguridad; la exposición de servicios a Internet forma parte del análisis de riesgos exigido por el RGPD. Las técnicas de escaneo y prueba se aplican exclusivamente sobre sistemas propios o con autorización escrita.

Errores Comunes y Consejos

  • ufw enable antes de permitir SSH. El error clásico y el más caro. Permitir siempre primero, habilitar después, y con la consola de la VM abierta.
  • Mezclar ufw y /etc/nftables.conf. Dos conjuntos de reglas compitiendo y un flush ruleset que borra las del otro en cada arranque. Elige uno.
  • Bloquear todo ICMP. Rompe el descubrimiento de MTU y provoca conexiones que se cuelgan en transferencias grandes. Permite al menos destination-unreachable y time-exceeded.
  • Olvidar iif lo accept. Media máquina deja de funcionar: túneles SSH, bases de datos por socket TCP local, procesos que se hablan por 127.0.0.1.
  • Reglas sin comment. Dentro de seis meses nadie se atreverá a borrar una regla que no sabe para qué está, y el ruleset solo crecerá.
  • log sin limit rate. Un escaneo te llena /var/log en minutos, y quedarte sin disco es una caída autoinfligida. Y no confíes solo en el firewall para lo interno: la aplicación debería escuchar únicamente en 127.0.0.1 de todas formas.
  • Consejo: guarda /etc/nftables.conf o la salida de ufw status numbered en git, y añade la comprobación de que el firewall está activo a revision_salud.sh.

Ejercicios

  1. La política completa desde cero. Escribe la secuencia exacta de comandos ufw para aplicar la política de la tabla del apartado 12 en una máquina remota, en el orden correcto, incluyendo las comprobaciones antes y después.
  2. Regla que ufw no expresa bien. Marta pide que solo portatil-luis (10.0.2.30) pueda acceder al 8080 para pruebas, con un máximo de 10 conexiones nuevas por minuto. Escribe la regla de nftables y explica por qué ufw se queda corto.
  3. Autobloqueo. Luis llama: no puede entrar por SSH desde 10.0.2.77 y dice que «el servidor le ha echado». Diagnostica el problema, resuélvelo y propón la corrección permanente.

Soluciones

1. Con la consola de la VM abierta y una segunda sesión SSH activa:

# Medir antes
$ sudo ufw status verbose > /root/ufw-antes.txt ; ss -tulpn | grep LISTEN
# Políticas por defecto
$ sudo ufw default deny incoming && sudo ufw default allow outgoing
$ sudo ufw default deny routed
# SSH PRIMERO, siempre; después el resto
$ sudo ufw limit 22/tcp comment 'SSH administracion'
$ sudo ufw allow 443/tcp comment 'HTTPS publico'
$ sudo ufw allow from 10.0.2.0/24 to any port 5432 proto tcp comment 'PostgreSQL interno'
# Habilitar y verificar
$ sudo ufw enable && sudo ufw status numbered
$ sudo diff -u /root/ufw-antes.txt <(sudo ufw status verbose)

Y la verificación que de verdad cierra el trabajo, desde portatil-alumno: abrir una sesión SSH nueva sin cerrar la anterior, y nmap -Pn -p 22,443,5432,8080 10.0.2.15 para comprobar que el 8080 aparece filtered. Fíjate en que el 8080 no necesita ninguna regla: la política deny incoming ya lo cubre. En una lista blanca, lo que no se nombra queda prohibido.

2. La regla en nftables, dentro de la cadena input y antes del log final:

        ip saddr 10.0.2.30 tcp dport 8080 ct state new \
            limit rate 10/minute burst 5 packets accept

ufw se queda corto por dos motivos: solo ofrece limit, con un umbral fijo (6 conexiones en 30 segundos) que no se puede ajustar sin editar sus plantillas internas, y no permite combinar en una sola regla origen concreto, puerto, estado de conexión y tasa personalizada. Aquí necesitamos exactamente eso. La alternativa dentro de ufw sería sudo ufw allow from 10.0.2.30 to any port 8080 proto tcp, que da el control de origen pero no la limitación de tasa.

Detalle importante: ct state new hace que la tasa se aplique solo a conexiones nuevas. Sin él, limitarías también los paquetes de una transferencia en curso y provocarías cortes aleatorios que parecerían un problema de red.

3. Diagnóstico en tres comandos:

$ sudo fail2ban-client status sshd | grep 'Banned IP list'
   `- Banned IP list:   203.0.113.44 10.0.2.77
$ sudo journalctl -u fail2ban --since "-1h" | grep 10.0.2.77
fail2ban.actions [1204]: NOTICE  [sshd] Ban 10.0.2.77

Luis ha fallado tres veces en diez minutos —probablemente con una clave equivocada o sin cargar en el agente— y la jaula sshd lo ha bloqueado 24 horas. Desbloqueo inmediato y corrección permanente:

$ sudo fail2ban-client set sshd unbanip 10.0.2.77
$ grep ignoreip /etc/fail2ban/jail.local
ignoreip  = 127.0.0.1/8 ::1 10.0.2.0/24

Y aquí está la lección de verdad: ignoreip ya incluía 10.0.2.0/24, así que si el bloqueo se produjo es porque el fichero se editó sin recargar el servicio, o porque la línea se escribió en jail.conf en lugar de en jail.local y una actualización del paquete se la llevó. La corrección permanente es sudo fail2ban-client reload tras cada cambio, verificar con sudo fail2ban-client get sshd ignoreip, y añadir esa comprobación a revision_salud.sh. La causa raíz de fondo es que Luis usa contraseña donde debería usar su clave ed25519: revisa su authorized_keys.

Conclusión

srv-tramontana ya no habla con cualquiera. Tiene una política de lista blanca escrita, aplicada en el orden correcto y verificada desde fuera con nmap: SSH limitado, HTTPS abierto, PostgreSQL restringido a la red interna y el 8080 invisible desde Internet a la espera del proxy inverso del Módulo 8. Sabes que debajo de ufw hay nftables y que debajo de nftables está netfilter; sabes escribir un ruleset inet completo con estado de conexión, conjuntos y limitación de tasa; has ajustado los sysctl que evitan suplantación, redirecciones e inundaciones SYN; y fail2ban bloquea automáticamente a 203.0.113.44 y a quien venga detrás, sin bloquearte a ti. El primero de los tres incidentes abiertos queda cerrado.

Y ahora la parte incómoda. Todo lo que has montado en estas tres lecciones es prevención, y la prevención falla: falla por un CVE que aún no tiene parche, por una credencial filtrada —como la de aquella copia con permisos 644—, por un despiste al abrir una regla «solo un momento» o porque alguien con acceso legítimo hace algo que no debería. El día que falle, tu firewall no te dirá nada: seguirá aplicando obedientemente las reglas mientras la conexión del atacante figura como established. La pregunta deja de ser «¿cómo impido que entren?» y pasa a ser «¿cómo me entero de que ya han entrado?». Eso es la lección 06-04: Sistemas de Detección de Intrusos, donde montarás AIDE para vigilar la integridad de los ficheros, pondrás auditd a seguir cada escritura sobre app.conf, buscarás las señales de compromiso en los logs, pasarás Lynis a tu servidor y aprenderás el procedimiento de respuesta a incidentes — incluida la regla que a nadie le gusta oír: un servidor comprometido se reinstala, no se limpia.

Curso de Linux: De Principiante a Administrador de Sistemas

Módulo 1: Introducción a Linux

Módulo 2: Comandos Básicos de Linux

Módulo 3: Habilidades Avanzadas en la Línea de Comandos

Módulo 4: Scripting en Shell

Módulo 5: Administración del Sistema

Módulo 6: Redes y Seguridad

Módulo 7: Temas Avanzados

Módulo 8: Proyectos Prácticos

© Copyright 2026. Todos los derechos reservados