Problema

En muchos homelabs el tráfico de dispositivos de confianza, invitados, IoT y servidores se mezcla en una única subred. Esa arquitectura simplifica la configuración inicial, pero rápidamente muestra sus límites: un dispositivo comprometido puede escanear la red, acceder a bases de datos internas o evadir las políticas de DNS. Cuando un adolescente experimenta con resolvers externos (DoH, DoT, VPN) el control de la red se vuelve casi imposible, y el tráfico de servicios críticos (NAS, CI/CD, bases de datos) queda expuesto a ataques internos o a filtraciones accidentales. La necesidad real es una segmentación clara, reglas de firewall estrictas entre zonas y una forma de obligar a todos los clientes a usar el resolver interno sin que puedan sobrescribirlo.

Causa

Los fallos habituales provienen de tres áreas:

  1. Falta de VLANs o VLANs mal definidas – Un solo broadcast domain permite que cualquier host descubra y ataque a los demás.
  2. Políticas de firewall por defecto “allow‑all” – Cuando las reglas solo filtran tráfico entrante desde WAN, el tráfico lateral entre zonas internas queda sin control.
  3. DNS sin forzar – Los clientes pueden cambiar su resolver a 1.1.1.1, Cloudflare DoH o cualquier VPN, lo que anula cualquier filtrado de contenido y permite la comunicación con servidores externos sin supervisión.

En entornos donde Docker comparte la red del host, los contenedores pueden saltarse la capa de firewall si se conectan directamente a la interfaz LAN. Además, la presencia de aplicaciones que usan DoH o ECH (Encrypted Client Hello) dificulta la detección de bypasses basados solo en inspección de DNS.

Solución

Una arquitectura basada en VLANs aisladas + firewall de zona en OpenWrt + redirección forzada a Pi-hole cubre la mayoría de los casos. El flujo de trabajo se divide en tres bloques:

1. Definir VLANs y asignar puertos

VLAN Nombre Subred Uso
1 LAN 10.0.1.0/24 Dispositivos de confianza
25 DMZ 10.0.25.0/24 Servidores expuestos a la LAN
30 Guest 10.0.30.0/24 Wi‑Fi de invitados
40 IoT 10.0.40.0/24 Dispositivos inteligentes

En el switch gestionado (TP‑Link TL‑SG108E) configura un trunk 802.1Q hacia el router OpenWrt y marca cada puerto de acceso con la VLAN correspondiente. En OpenWrt crea una interface por VLAN en /etc/config/network:

config interface 'lan'
    option ifname 'eth0.1'
    option proto 'static'
    option ipaddr '10.0.1.1/24'

config interface 'dmz'
    option ifname 'eth0.25'
    option proto 'static'
    option ipaddr '10.0.25.1/24'

config interface 'guest'
    option ifname 'eth0.30'
    option proto 'static'
    option ipaddr '10.0.30.1/24'

config interface 'iot'
    option ifname 'eth0.40'
    option proto 'static'
    option ipaddr '10.0.40.1/24'

2. Configurar zonas y reglas de firewall

Define zonas que reflejen la tabla de políticas:

config zone
    option name 'lan'
    option network 'lan'
    option input 'ACCEPT'
    option output 'ACCEPT'
    option forward 'REJECT'

config zone
    option name 'dmz'
    option network 'dmz'
    option input 'REJECT'
    option output 'ACCEPT'
    option forward 'REJECT'

config zone
    option name 'guest'
    option network 'guest'
    option input 'REJECT'
    option output 'ACCEPT'
    option forward 'REJECT'

config zone
    option name 'iot'
    option network 'iot'
    option input 'REJECT'
    option output 'ACCEPT'
    option forward 'REJECT'

Luego permite solo los flujos necesarios, por ejemplo NFS desde DMZ a LAN:

config rule
    option name 'Allow NFS DMZ→LAN'
    option src 'dmz'
    option dest 'lan'
    option dest_port '2049'
    option target 'ACCEPT'
    option proto 'tcp udp'

3. Forzar DNS mediante DNAT a Pi-hole

Cada zona redirige cualquier petición DNS (TCP/UDP 53) a la IP del Pi-hole en LAN (10.0.1.254). La regla se repite para cada zona:

config redirect
    option name 'Redirect-DNS-LAN'
    option src 'lan'
    option src_dport '53'
    option dest 'lan'
    option dest_ip '10.0.1.254'
    option dest_port '53'
    option proto 'tcp udp'
    option target 'DNAT'

config redirect
    option name 'Redirect-DNS-DMZ'
    option src 'dmz'
    option src_dport '53'
    option dest 'lan'
    option dest_ip '10.0.1.254'
    option dest_port '53'
    option proto 'tcp udp'
    option target 'DNAT'

# Repetir para guest e iot

Bloquea DoT (853) y cualquier tráfico DNS externo:

config rule
    option name 'Block DoT'
    option src '*'
    option dest_port '853'
    option target 'REJECT'
    option proto 'tcp udp'

4. Aislar contenedores Docker

Cada stack usa su propia red de Docker (docker composenetworks:) y solo el contenedor de Caddy se conecta a la red lan para exponer puertos. Los demás permanecen en redes internas (dmz_net, guest_net, etc.) y nunca aparecen en la tabla de enrutamiento de la LAN. Un sidecar de Tailscale en los servicios que requieren acceso remoto mantiene la superficie de ACL dentro del contenedor.

5. Gestión de secretos y CI/CD

Almacena variables sensibles en archivos .env.sops cifrados con age. El pipeline de Forgejo Actions:

  1. Checkout del repo.
  2. Decrypt .env.sops.env.
  3. docker compose pull && docker compose up -d.
  4. Health‑check; rollback automático si falla.

Este flujo garantiza que los cambios de configuración no introduzcan brechas de red.

Cuándo aplicar esta solución

  • Homelabs con varios tipos de dispositivos (PC, IoT, invitados) que comparten un único router.
  • Entornos donde se ejecutan servicios críticos (NAS, CI/CD, bases de datos) en Docker y se necesita aislamiento sin hardware adicional.
  • Situaciones donde usuarios internos intentan evadir DNS mediante DoH, VPN o resolvers externos.

No es necesario cuando la red es extremadamente pequeña (menos de 5 dispositivos) o cuando se dispone de un firewall de nivel empresarial con inspección profunda de paquetes (DPI) que ya controla el DNS.

Código

# OpenWrt UCI redirect para forzar DNS en la zona IoT
uci add firewall redirect
uci set firewall.@redirect[-1].name='Redirect-DNS-IoT'
uci set firewall.@redirect[-1].src='iot'
uci set firewall.@redirect[-1].src_dport='53'
uci set firewall.@redirect[-1].dest='lan'
uci set firewall.@redirect[-1].dest_ip='10.0.1.254'
uci set firewall.@redirect[-1].dest_port='53'
uci set firewall.@redirect[-1].proto='tcp udp'
uci set firewall.@redirect[-1].target='DNAT'
uci commit firewall
/etc/init.d/firewall restart
# docker‑compose.yml fragmento para una stack aislada
networks:
  dmz_net:
    driver: bridge
    ipam:
      config:
        - subnet: 10.0.25.0/24

services:
  immich:
    image: immich/immich-server:latest
    networks:
      - dmz_net
    environment:
      - TZ=Europe/Madrid
    restart: unless-stopped

Verificación

  1. Desde un cliente en la VLAN IoT, ejecutar dig @10.0.1.254 example.com. La respuesta debe venir del Pi-hole.
  2. Intentar dig @1.1.1.1 example.com; la petición debe ser redirigida a 10.0.1.254 y aparecer en los logs de Pi-hole.
  3. Probar acceso a NFS desde DMZ: showmount -e 10.0.1.1. Solo el puerto 2049 debe estar abierto.
  4. Verificar que los contenedores que no están en la red lan no aparecen en iptables -L -v -n bajo la cadena FORWARD de la zona LAN.
  5. Revisar en la UI de Pi-hole que todas las consultas provienen de las subredes esperadas.

Notas adicionales

  • Sidecar de Tailscale: montar el socket dentro del contenedor evita que el host exponga la interfaz de red completa a Internet.
  • DoH canary: bloquear dominios como use-application-dns.net ayuda a detectar clientes que intentan usar resolvers externos.
  • AppArmor y seccomp en Docker reducen la superficie de ataque de los contenedores; aplica perfiles mínimos y habilita no-new-privileges.
  • MoCA para backhaul: si la infraestructura de cable coaxial está disponible, los adaptadores MoCA de 2.5 GbE ofrecen una alternativa cableada sin romper paredes.
  • Monitoreo de flujo: aunque el NetFlow v9 quedó en desuso, conservar los logs de firewall en logread permite auditorías rápidas sin necesidad de un colector externo.