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:
- Falta de VLANs o VLANs mal definidas – Un solo broadcast domain permite que cualquier host descubra y ataque a los demás.
- 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.
- 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 compose networks:) 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:
- Checkout del repo.
- Decrypt
.env.sops→.env. docker compose pull && docker compose up -d.- 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
- Desde un cliente en la VLAN IoT, ejecutar
dig @10.0.1.254 example.com. La respuesta debe venir del Pi-hole. - 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. - Probar acceso a NFS desde DMZ:
showmount -e 10.0.1.1. Solo el puerto 2049 debe estar abierto. - Verificar que los contenedores que no están en la red lan no aparecen en
iptables -L -v -nbajo la cadenaFORWARDde la zona LAN. - 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.netayuda 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
logreadpermite auditorías rápidas sin necesidad de un colector externo.