Problema

En muchos entornos de homelab o dispositivos embebidos, el sistema operativo base no incluye una política de firewall activa. Las cadenas de iptables o nftables aparecen con la política ACCEPT y no hay reglas que limiten puertos expuestos. Cuando se despliegan contenedores Docker, el tráfico DNAT se dirige a la cadena FORWARD, de modo que una regla solo en INPUT no protege los puertos publicados. El resultado típico es que cualquier servicio que escuche en 0.0.0.0 queda accesible desde la LAN (SMB, NFS, VNC, etc.) y, si el host está conectado a una VPN, también desde Internet sin una barrera clara.

El síntoma más frecuente es la aparición inesperada de puertos abiertos en escaneos de red, o la imposibilidad de conectar a un contenedor porque la regla de bloqueo se aplicó después de que Docker ya había creado la redirección. En entornos gestionados por SSH, aplicar reglas manualmente puede bloquear la propia sesión y dejar el host inaccesible.

Causa

  1. Política por defecto abierta – La mayoría de distribuciones de bajo consumo configuran iptables -P INPUT ACCEPT, FORWARD ACCEPT, OUTPUT ACCEPT. Sin una capa de filtrado, cualquier proceso que abra un socket en todas las interfaces queda expuesto.
  2. Ignorar la cadena DOCKER‑USER – Docker inserta sus propias reglas en FORWARD y permite que el usuario añada filtros en DOCKER‑USER. Si el firewall solo revisa INPUT, el tráfico DNAT a puertos publicados pasa sin control.
  3. Falta de automatismo de rollback – Cambiar reglas a través de una sesión SSH sin un mecanismo de reversión puede dejar al host sin acceso si la regla bloquea la propia conexión.
  4. Interfaces de túneles (Tailscale, ZeroTier) no excluidas – Al no crear excepciones para interfaces de VPN, los usuarios pierden conectividad remota al aplicar una política restrictiva.

Estos factores aparecen de forma recurrente en dispositivos como ZimaBoard, placas ARM con Linux minimalista, o cualquier instalación de Docker sobre una distro sin firewall preconfigurado.

Solución

Implementar un firewall host‑only que:

  • Filtre en INPUT para daemons locales.
  • Filtre en DOCKER‑USER para puertos publicados por contenedores.
  • Utilice una lista blanca (allowlist) por defecto y una lista negra (blocklist) para Docker.
  • Excluya siempre lo, la IP del host y las interfaces de túnel (tailscale0, zt0, etc.).
  • Safe‑Apply: aplicar cambios en una transacción temporal, iniciar un temporizador de 120 s y revertir automáticamente si no se confirma la nueva política.

Paso a paso general

  1. Crear un script de generación de reglas que lea un archivo de configuración sencillo (YAML o JSON) con puertos permitidos y bloques explícitos.

  2. Construir la cadena INPUT:

    iptables -N ZFW_INPUT
    iptables -F ZFW_INPUT
    iptables -A ZFW_INPUT -i lo -j ACCEPT
    iptables -A ZFW_INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
    iptables -A ZFW_INPUT -s 10.0.0.0/8 -i tailscale0 -j ACCEPT   # ejemplo VPN
    # allowlist
    iptables -A ZFW_INPUT -p tcp --dport 22 -j ACCEPT
    iptables -A ZFW_INPUT -j DROP
    iptables -I INPUT -j ZFW_INPUT
    
  3. Construir la cadena DOCKER‑USER:

    iptables -N ZFW_DOCKER
    iptables -F ZFW_DOCKER
    iptables -A ZFW_DOCKER -i lo -j ACCEPT
    iptables -A ZFW_DOCKER -i tailscale0 -j ACCEPT
    # blocklist (ejemplo)
    iptables -A ZFW_DOCKER -p tcp --dport 5900 -j REJECT --reject-with tcp-reset
    iptables -A ZFW_DOCKER -j ACCEPT
    iptables -I DOCKER-USER -j ZFW_DOCKER
    
  4. Implementar Safe‑Apply:

    • Copiar la tabla actual a una regla de respaldo (iptables-save > /tmp/iptables.backup).
    • Aplicar la nueva tabla (iptables-restore < /tmp/iptables.new).
    • Iniciar un sleep 120 && iptables-restore < /tmp/iptables.backup en background.
    • Confirmar la aplicación (por ejemplo, touch /tmp/zfw.commit && kill $PID).

    El script puede exponer un endpoint HTTP sencillo o un comando zfw commit que elimine el proceso de rollback.

  5. Persistencia – En sistemas con systemd, crear un servicio zfw.service que ejecute el script al arranque y mantenga el proceso de rollback activo.

Cuándo aplicar esta solución

  • Entornos sin firewall – Cualquier host Linux que arranca con políticas ACCEPT y ejecuta Docker.
  • Acceso remoto por SSH – Cuando la única vía de gestión es una sesión remota y se necesita evitar bloqueos accidentales.
  • VPN obligatoria – Si la conectividad depende de túneles como Tailscale o ZeroTier, la excepción de interfaces es esencial.
  • No es necesario – En routers o firewalls perimetrales donde ya existe una política de borde; aquí la solución sería redundante.

Código

#!/usr/bin/env bash
set -euo pipefail

# Ruta a los archivos temporales
NEW_RULES="/tmp/iptables.new"
BACKUP_RULES="/tmp/iptables.backup"
ROLLBACK_PID=""

apply_rules() {
    iptables-save > "$BACKUP_RULES"
    iptables-restore < "$NEW_RULES"
    # Inicia rollback automático
    (sleep 120 && iptables-restore < "$BACKUP_RULES" && echo "Rollback ejecutado") &
    ROLLBACK_PID=$!
    echo "Reglas aplicadas. Confirme con 'zfw commit' antes de 120 s."
}

commit() {
    if [[ -n "$ROLLBACK_PID" ]]; then
        kill "$ROLLBACK_PID" 2>/dev/null || true
        echo "Commit confirmado, rollback cancelado."
    else
        echo "No hay rollback pendiente."
    fi
}

case "${1:-}" in
    apply) apply_rules ;;
    commit) commit ;;
    *) echo "Uso: $0 {apply|commit}" ;;
esac

Verificación

  1. Escaneo de puertos – Ejecutar nmap -p- <IP_HOST> antes y después de aplicar las reglas; los puertos no listados en la allowlist deben aparecer filtrados.
  2. Conexión SSH – Abrir una sesión SSH, lanzar zfw apply, esperar menos de 120 s y ejecutar zfw commit. Si la sesión se mantiene, la regla es válida.
  3. Docker publish – Crear un contenedor docker run -d -p 8080:80 nginx. Verificar que iptables -L DOCKER-USER -n -v muestra la regla ACCEPT para 8080 solo si está en la allowlist.
  4. VPN – Conectarse vía Tailscale, intentar acceder al host desde la IP de la VPN; la regla de excepción debe permitir el tráfico.

Notas adicionales

  • Persistencia de Docker – Docker recrea la cadena DOCKER‑USER al reiniciar el daemon. Mantener la regla -I DOCKER-USER -j ZFW_DOCKER en un script de systemd garantiza que la cadena personalizada siempre esté presente.
  • IPv6 – Si el host usa IPv6, duplicar las reglas con ip6tables o migrar a nftables y crear tablas separadas para ip y ip6.
  • Backup/restore – Guardar iptables-save en /etc/iptables/rules.v4 permite restaurar rápidamente después de una actualización del kernel.
  • Per‑container binding – Para aislar contenedores individualmente, añada una etiqueta al contenedor (--label zfw.allow=22) y haga que el script genere reglas basadas en docker inspect.
  • Multi‑host – En clústers, sincronizar la configuración mediante GitOps (por ejemplo, ArgoCD) evita divergencias entre nodos.

Con este enfoque, cualquier servidor Linux que arranque sin firewall gana una capa de protección mínima pero fiable, mientras que la lógica de safe‑apply elimina el miedo a bloquearse a sí mismo durante la configuración.