Problema

Muchos entornos de homelab o servidores de bajo consumo se entregan con una pila de red sin políticas restrictivas: todas las cadenas de iptables están en ACCEPT, no existe un conjunto de reglas nftables y herramientas como UFW no están instaladas. Cuando el host ejecuta servicios que escuchan en 0.0.0.0 (SMB, NFS, rpcbind, VNC, etc.) o lanza contenedores Docker con puertos publicados, esos puertos quedan expuestos a la LAN sin filtro alguno. El síntoma típico es que cualquier máquina en la red local puede conectar a esos servicios sin autenticación adicional, lo que rompe la premisa de “solo los servicios que yo configure”. Además, aplicar reglas de iptables a través de una sesión SSH puede dejar al administrador fuera si la regla bloquea la propia conexión.

Causa

  1. Configuración predeterminada abierta – Distribuciones minimalistas o imágenes de hardware embebido suelen dejar policy ACCEPT en INPUT, FORWARD y OUTPUT. Sin una política de denegación, cualquier socket que se abra es accesible.
  2. Ignorar la cadena DOCKER‑USER – Docker inserta reglas de NAT en FORWARD. Los firewalls que solo filtran INPUT no interceptan el tráfico DNAT que llega a contenedores publicados, por lo que los puertos siguen abiertos aunque la política de INPUT sea DROP.
  3. Falta de mecanismo de prueba/rollback – Los administradores aplican cambios directamente, sin validar que la nueva regla no interfiere con la sesión activa. Cuando la regla es errónea, la única salida es reiniciar el host o perder la conexión.
  4. Ausencia de gestión centralizada – Sin una UI o archivo de configuración, cada regla se escribe a mano, lo que genera inconsistencias y dificulta auditorías.

Solución

Implementar un firewall de host que:

  • Cambie la política por defecto a DROP en INPUT.
  • Utilice la cadena DOCKER‑USER para filtrar tráfico hacia contenedores publicados.
  • Mantenga una lista blanca de interfaces de gestión (loopback, interfaces VPN como tailscale0 o zerotier0).
  • Aplique cambios mediante un proceso “Safe‑Apply”: carga temporal de reglas, espera de confirmación y rollback automático si no se confirma dentro de un plazo.

Paso a paso

  1. Crear archivo de reglas base
    Un script que defina la política y las excepciones estáticas. Se guarda, por ejemplo, en /etc/firewall/base.rules.

  2. Añadir regla de Docker
    Insertar una regla al principio de DOCKER‑USER que redirija todo el tráfico a una política de DROP y luego permita explícitamente los puertos que el administrador desea exponer.

  3. Implementar Safe‑Apply
    Un wrapper que:

    • Carga las nuevas reglas con iptables-restore --noflush.
    • Inicia un temporizador (120 s) que, al expirar, restaura la copia anterior usando iptables-restore.
    • Permite confirmar la aplicación mediante zfw commit o una petición HTTP a una UI ligera.
  4. Persistencia
    Configurar systemd para ejecutar el script de carga en arranque y para restaurar la última configuración confirmada.

  5. Auditoría básica
    Añadir una regla de logging en INPUT y DOCKER‑USER que registre paquetes descartados con LOG --log-prefix "[FW DROP]". Los logs pueden ser enviados a journalctl o a un syslog central.

Cuándo aplicar esta solución

  • Entornos sin firewall – Servidores que arrancan con policy ACCEPT y que ejecutan servicios expuestos.
  • Uso intensivo de Docker – Cuando se publican puertos de contenedores y se necesita que el tráfico pase por el mismo control que el host.
  • Acceso remoto por SSH – Si la única vía de gestión es SSH, el mecanismo Safe‑Apply evita bloqueos accidentales.
  • No se requiere router/edge firewall – La solución actúa solo en la frontera del host; para filtrado de tráfico entre subredes se necesita un dispositivo dedicado.

No es adecuada cuando se busca inspección profunda de paquetes (IDS/IPS) o gestión de políticas entre varios hosts simultáneamente.

Código

#!/usr/bin/env bash
# firewall-apply.sh – carga segura de reglas iptables

RULES_NEW="/etc/firewall/rules.new"
RULES_OLD="/etc/firewall/rules.backup"
TIMEOUT=120

# Guardar estado actual
iptables-save > "$RULES_OLD"

# Intentar cargar nuevas reglas sin flush
if ! iptables-restore --noflush < "$RULES_NEW"; then
    echo "Error al cargar reglas nuevas"
    exit 1
fi

# Iniciar temporizador de rollback
(
    sleep "$TIMEOUT"
    echo "Rollback automático: restaurando reglas anteriores"
    iptables-restore < "$RULES_OLD"
) &

ROLLBACK_PID=$!

# Esperar confirmación del usuario
read -p "Confirmar aplicación de reglas (s/N): " -r CONFIRM
if [[ "$CONFIRM" =~ ^[Ss]$ ]]; then
    kill "$ROLLBACK_PID" 2>/dev/null
    echo "Reglas confirmadas"
else
    echo "No se confirmó, ejecutando rollback"
    iptables-restore < "$RULES_OLD"
fi

Verificación

  1. Comprobar política

    iptables -L INPUT -v -n
    

    La columna policy debe mostrar DROP.

  2. Probar puerto de Docker
    Desde otra máquina, intenta conectar al puerto publicado. Si el puerto no está en la whitelist, la conexión debe ser rechazada y aparecer una entrada en el log con el prefijo [FW DROP].

  3. Validar rollback
    Ejecuta firewall-apply.sh y no confirmes. Después de 120 s, verifica que la tabla vuelva a la configuración anterior con iptables-save.

  4. Revisar logs

    journalctl -u firewall-apply -k | grep "\[FW DROP\]"
    

Notas adicionales

  • Orden de reglas – En DOCKER‑USER la regla de DROP debe estar antes que cualquier regla de aceptación, de lo contrario Docker insertará sus propias reglas por encima.
  • IPv6 – Si el host usa IPv6, replica la misma lógica en ip6tables. La ausencia de reglas IPv6 es una fuente frecuente de fugas.
  • Backup de reglas – Programar una tarea cron que copie iptables-save a /var/backups/iptables-$(date +%F).bak facilita la recuperación después de un error inesperado.
  • Compatibilidad con nftables – En sistemas que prefieren nftables, la lógica es idéntica: usar la tabla filter, cadena input con policy drop y la cadena docker-user (creada con nft add chain ip filter docker-user { type filter hook prerouting priority 0 \; }).
  • Limitaciones de Safe‑Apply – El temporizador solo protege la sesión que ejecuta el script. Si la regla bloquea todo el tráfico entrante, incluso el propio proceso de rollback seguirá activo porque se ejecuta en el mismo espacio de red. Por eso se usa --noflush y se guarda el estado previo antes de aplicar cambios.