Volver al blog

Montar un homelab desde cero: las redes - Homelab (02/06)

Caso realHomelabRedesWireGuard

En el Post 1 vimos el panorama general: cuatro nodos Proxmox, un NAS, unos cuantos VPS y dos clústeres de Kubernetes. Pero nada de eso sirve si no se pueden hablar entre sí.

Este post va sobre la capa de red — cómo se mueve el tráfico entre dos ubicaciones físicas, cómo los servidores en la nube acceden al homelab sin abrir ningún puerto, y cómo los usuarios de internet llegan a servicios como zetesis.xyz. Veremos WireGuard, Tailscale, Cloudflare y Caddy, y la base sobre la que corre todo: los routers VyOS, el BGP entre los clústeres y el borde, y el sistema de subredes y DNS.

Si las redes no son lo tuyo, tranquilo. Empezaré desde lo básico.

El problema

Esto es lo que hay que resolver:

  1. Dos ubicaciones físicas (Atenas y Roma) necesitan compartir recursos como si fuesen una sola red
  2. Los VPS en la nube necesitan alcanzar servicios internos sin exponerlos a internet
  3. Los usuarios de internet necesitan llegar a aplicaciones web alojadas dentro del homelab
  4. Todos los servidores necesitan hacer backup a un NAS que vive en una de las redes internas

Y hay que hacerlo sin abrir ni un solo puerto en el router de casa. El port forwarding implica exponer tu IP y tus servicios directamente a internet — un riesgo de seguridad que se puede evitar por completo.

Capa 1: túnel WireGuard sitio a sitio

WireGuard es un protocolo VPN (Virtual Private Network). Una VPN es, simplificando, un túnel cifrado entre dos puntos — los datos entran por un extremo y salen por el otro, y nadie en medio puede leerlos.

Las dos redes locales tienen cada una un router VyOS (un sistema operativo de red libre basado en Debian, pensado para línea de comandos y automatización). Estos routers mantienen un túnel WireGuard permanente:

Router — Red — Subred LAN — IP WireGuard — DNS dinámico

temistocles — Atenas — 10.1.0.0/24 — 10.200.0.10 — atenas.zetesis.localhost

constantino — Roma — 10.0.0.0/24 — 10.200.0.1 — roma.zetesis.localhost

Ambas ubicaciones tienen internet residencial con IPs dinámicas (la IP pública cambia periódicamente). En cada sitio corre un servicio de DDNS (DNS dinámico) llamado marco-polo que mantiene un hostname como roma.zetesis.localhost apuntando a la IP pública actual. Los routers usan esos hostnames para encontrarse.

Loading diagram...

El túnel WireGuard crea una tercera red virtual (10.200.0.0/24) que comparten ambos routers. Cada router sabe reenviar el tráfico destinado a la subred del otro sitio a través del túnel. Así, un dispositivo en Atenas (10.1.0.x) puede alcanzar uno en Roma (10.0.0.x) de forma transparente — funciona como si estuviesen en la misma LAN.

Configuración en VyOS, como código

Los routers no se configuran a mano por consola: su configuración nace en el repositorio GitOps. Cada router se compone de fragmentos — uno de firewall compartido y otros propios de interfaces, NAT, protocolos, servicios y sistema — que se ensamblan y renderizan con render.sh a un fichero .boot completo. Los secretos (claves WireGuard, tokens) se inyectan desde SOPS solo en el momento del render, nunca en las plantillas.

El flujo de trabajo:

  1. Editas los fragmentos o el router.env del router correspondiente.
  2. render.sh genera el .boot y lo valida — variables sin resolver, placeholders vacíos o llaves desbalanceadas hacen fallar el build; la CI renderiza ambos routers con valores de prueba en cada pull request.
  3. Aplicas el resultado y, para cambios de riesgo, usas commit-confirm: el router revierte solo si no confirmas a tiempo.

El router se convierte así en un despliegue más: su estado deseado vive en Git y es reproducible desde cero. La configuración del túnel en temistocles queda así:

Interfaces:

  • WAN (vtnet0) — obtiene su IP por DHCP del ISP
  • LAN (vtnet1) — IP estática 10.1.0.1/24, la puerta de enlace de todos los dispositivos de la red Atenas

Instancia local WireGuard (wg0):

  • Puerto de escucha: 51820
  • Dirección del túnel: 10.200.0.10/24

Peer WireGuard (Roma):

  • Endpoint: roma.zetesis.localhost:51820 (el hostname DDNS)
  • Allowed IPs: 10.0.0.0/24, 10.200.0.0/24
  • Keepalive: 25 segundos

Lo esencial: el tráfico hacia 10.0.0.0/24 (la LAN de Roma) o 10.200.0.0/24 (la red WireGuard) se enruta por el túnel. El keepalive envía un paquete cada 25 segundos para mantener el túnel vivo a través del NAT (Network Address Translation) — algo necesario cuando ambos lados tienen internet residencial.

El router constantino tiene la configuración espejo, con un peer llamado "Atenas" apuntando a atenas.zetesis.localhost.

Firewall por interfaces

VyOS lleva un firewall basado en interfaces. Cada interfaz (LAN, WAN, WireGuard) tiene su propio conjunto de reglas que controlan qué tráfico puede pasar:

Reglas WAN:
  - Bloquear todo el tráfico entrante (excepto UDP 51820 para WireGuard)

Reglas LAN:
  - Permitir todo el tráfico saliente hacia WAN
  - Permitir todo el tráfico saliente hacia WireGuard (wg0)

Reglas WireGuard (wg0):
  - Permitir tráfico desde 10.0.0.0/24 y 10.200.0.0/24 hacia LAN
  - Bloquear todo lo demás

El principio es sencillo: la LAN puede llegar a todas partes, la WAN no puede llegar a nada (excepto WireGuard), y los peers WireGuard solo pueden llegar a la LAN desde subredes conocidas. Mucho más restrictivo que la configuración por defecto, que permitía tráfico libre entre todos los peers.

DHCP, subredes y DNS

Cada router también hace de servidor DHCP para su red, con una IP estática mapeada a la dirección MAC de cada dispositivo:

dhcp-server {
    shared-network-name LAN {
        subnet 10.1.0.0/24 {
            default-router 10.1.0.1
            static-mapping socrates  { ip-address 10.1.0.10 }
            static-mapping spinoza   { ip-address 10.1.0.11 }
        }
    }
}

El mapa completo de subredes queda así:

  • 10.0.0.0/24 — Roma (producción)
  • 10.1.0.0/24 — Atenas
  • 10.200.0.0/24 — la red virtual del túnel WireGuard
  • 10.0.120.0/24 — la red de plataforma de pizarro: gateway en .1, nodo en .10 y un rango alto reservado para balanceo de carga

El DNS va más lejos que reenviar consultas a 1.1.1.1. En Roma la cadena completa es:

clientes → pdns (10.0.0.1, caché en el router)
         → unbound (contenedor en el propio router: filtro RPZ + validación DNSSEC)
         → Quad9 por DNS-over-TLS

unbound corre como contenedor dentro de constantino y hace dos cosas notables. Primero, aplica un filtro RPZ (Response Policy Zone, una zona de respuestas manipuladas a propósito): los dominios de publicidad y rastreo se resuelven a una dirección muerta, así el bloqueo de anuncios ocurre a nivel de DNS para toda la red, sin instalar nada en los dispositivos. Segundo, valida DNSSEC localmente con las anclas de la raíz, y solo sale a internet por DoT (DNS over TLS) contra Quad9 — las consultas viajan cifradas incluso entre el router y el resolver público.

El reenvío acepta consultas de ambas redes — así los dispositivos de Roma pueden resolver DNS a través del router de Atenas vía el túnel WireGuard, y viceversa.

BGP: los servicios de Kubernetes, alcanzables desde la LAN

Dentro de un clúster Kubernetes, una ClusterIP solo existe dentro del clúster. Los Services de tipo LoadBalancer necesitan que alguien les asigne una IP externa — y que el resto de la red aprenda a llegar a ella.

La respuesta es que los clústeres hablan eBGP (Border Gateway Protocol, el protocolo de encaminamiento entre sistemas autónomos de internet) con el router del sitio, a través de Cilium, el CNI que les da red. Cada uno tiene su número de sistema autónomo:

  • constantino (el router) — AS64512
  • cortes — AS64513
  • pizarro — AS64514, emparejado con constantino a través de su red de plataforma

Las reglas del juego son estrictas: solo los Services explícitamente etiquetados para anunciarse reciben IP del pool de balanceo —en pizarro, el rango alto 10.0.120.128/25 de su /24—, y el clúster la anuncia al router como una ruta de host /32. Sin extensión de capa 2 ni túneles: el router aprende la ruta y cualquier dispositivo de la LAN alcanza el servicio directamente, sin NodePort ni port forwarding.

Los temporizadores son conservadores —30 segundos de hold, 10 de keepalive, graceful restart— y ambos extremos exportan sus métricas BGP al hub de observabilidad: si una sesión cae, salta en Grafana.

Capa 2: Tailscale, VPN en malla

WireGuard conecta las dos ubicaciones físicas, pero los VPS en la nube necesitan otro enfoque. Están alojados en distintos proveedores, no tienen IPs estáticas, y no queremos gestionar túneles punto a punto entre cada par de servidores.

Ahí entra Tailscale. Es una VPN en malla construida sobre WireGuard. En vez de configurar cada conexión a mano, instalas Tailscale en cada dispositivo y se encuentran y conectan automáticamente a través de un servidor de coordinación. Cada dispositivo recibe una IP estable en la red Tailscale (100.x.x.x) y puede alcanzar directamente a cualquier otro.

Tres casos de uso en el homelab

1. Acceso de los VPS a spinoza para backup

Todos los VPS corren un agente de backup (tolstoi) que necesita alcanzar MinIO en spinoza (10.1.0.11:9000). Pero spinoza está en la LAN de Atenas — no es accesible directamente desde internet.

La solución: spinoza corre Tailscale y se anuncia como subnet router, haciendo 10.1.0.0/24 accesible desde la tailnet. Cada VPS corre un contenedor sidecar llamado cervantes — un cliente Tailscale con --accept-routes que se une a la malla y obtiene acceso a spinoza. El contenedor de backup comparte la pila de red de cervantes:

# services/tolstoi/docker-compose.yaml (configuración base)
services:
  resticker-base:
    image: mazzolino/restic:1.8.2
    environment:
      RESTIC_REPOSITORY: s3:http://10.1.0.11:9000/deployment-restic
      BACKUP_CRON: "0 3 * * *"
    # comparte red con cervantes

  cervantes-s3-gateway-base:
    image: tailscale/tailscale:v1.94.2
    environment:
      - TS_STATE_DIR=/var/lib/tailscale
      - TS_AUTHKEY=${TS_AUTHKEY}
      - TS_EXTRA_ARGS=--accept-routes
      - TS_USERSPACE=false
    devices:
      - /dev/net/tun:/dev/net/tun
    cap_add:
      - net_admin

La línea network_mode: "service:cervantes-s3-gateway" en cada despliegue hace que el contenedor de backup enrute todo su tráfico a través de cervantes. Desde la perspectiva del agente de backup, 10.1.0.11:9000 es una IP normal — Tailscale gestiona el enrutamiento de forma transparente.

2. Enrutamiento de von-braun a los clústeres Kubernetes

El VPS von-braun corre Caddy (el reverse proxy) y necesita reenviar tráfico web a los clústeres Kubernetes dentro del homelab. Usa Tailscale para alcanzar Traefik en el clúster cortes.

Fíjate en la configuración DNS del contenedor Caddy:

services:
  caddy:
    dns:
      - 100.100.100.100    # MagicDNS de Tailscale
      - 1.1.1.1            # Fallback de Cloudflare

100.100.100.100 es el resolver DNS integrado de Tailscale. Resuelve hostnames de Tailscale (como zetesis-prod-typesense-api) a sus direcciones 100.x.x.x. El Caddy de von-braun reenvía el tráfico de zetesis.xyz por Tailscale hasta el clúster cortes.

3. Servicios Kubernetes expuestos vía Tailscale

Dentro de los clústeres, el operador de Tailscale puede exponer Services e Ingresses directamente a la tailnet. Hay dos patrones.

Para servicios TCP como PostgreSQL, se usan anotaciones en el Service:

apiVersion: v1
kind: Service
metadata:
  name: postgres-tailscale
  annotations:
    tailscale.com/expose: "true"
    tailscale.com/hostname: "zetesis-prod-postgres"
    tailscale.com/tags: "tag:cortes"
spec:
  selector:
    cnpg.io/cluster: postgres
    role: primary
  ports:
    - port: 5432

Esto crea un nodo Tailscale llamado zetesis-prod-postgres que hace de proxy TCP hacia el pod de PostgreSQL. Cualquier dispositivo en la tailnet puede conectarse — útil para herramientas de gestión de base de datos desde el portátil.

Para servicios HTTP que necesitan TLS, se usa un Ingress de Tailscale:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: typesense-api
  annotations:
    tailscale.com/tags: "tag:cortes"
spec:
  ingressClassName: tailscale
  tls:
    - hosts:
        - zetesis-prod-typesense-api

Tailscale provisiona un certificado TLS automáticamente y hace el servicio accesible en https://zetesis-prod-typesense-api.tailf1ac6e.ts.net.

ACL: quién puede hablar con quién

Tailscale soporta listas de control de acceso (ACL) — reglas que restringen qué dispositivos pueden alcanzar a cuáles. La política ACL vive en Git (tailscale/policy.hcl) y se despliega automáticamente con GitHub Actions:

"grants": [
    // El admin tiene acceso total
    { "src": ["autogroup:admin"], "dst": ["*"], "ip": ["*"] },

    // Los VPS solo pueden alcanzar MinIO para backups
    {
        "src": ["tag:von-braun", "tag:escohotado", "tag:unamuno"],
        "dst": ["10.1.0.11"],
        "ip":  ["9000"],
    },

    // von-braun puede alcanzar los clústeres K8s para proxying HTTP
    { "src": ["tag:von-braun"], "dst": ["tag:cortes"], "ip": ["80", "443"] },
]

Mínimo privilegio: cada VPS solo puede alcanzar exactamente los puertos que necesita. Si un VPS se ve comprometido, el atacante no puede pivotar al resto del homelab — solo puede llegar a MinIO en el puerto 9000 o a HTTP en los clústeres.

La GitHub Action se ejecuta con cada push que modifique el fichero de política:

# .github/workflows/tailscale-acl.yml
- name: Deploy ACL
  if: github.ref == 'refs/heads/main'
  uses: tailscale/gitops-acl-action@v1
  with:
    api-key: ${{ secrets.TAILSCALE_API_KEY }}
    tailnet: ${{ secrets.TAILSCALE_TAILNET }}
    policy-file: tailscale/policy.hcl
    action: apply

En pull requests se ejecuta en modo test (dry-run) para validar la política sin aplicarla.

Capa 3: Cloudflare

Cloudflare se sitúa entre internet y el homelab. Cumple tres funciones: gestiona el DNS de todos los dominios *.zetesis.localhost y *.zetesis.xyz; absorbe tráfico malicioso antes de que llegue a los servidores (protección DDoS); y oculta las IPs reales de los servidores haciendo de proxy — los usuarios se conectan al edge de Cloudflare, que a su vez conecta con el origen.

DNS dinámico con marco-polo

Las dos ubicaciones físicas tienen IPs dinámicas. Cada una corre un servicio llamado marco-polo que mantiene los registros DNS de Cloudflare actualizados:

services:
  cloudflare-ddns:
    image: favonia/cloudflare-ddns:1.15.1
    network_mode: host
    read_only: true
    cap_drop: [all]
    cap_add: [SETUID, SETGID]
    environment:
      - CLOUDFLARE_API_TOKEN=${CF_API_TOKEN}
      - DOMAINS=roma.zetesis.localhost
      - PROXIED=true

Este contenedor comprueba la IP pública cada pocos minutos y actualiza el registro DNS si ha cambiado. El flag PROXIED=true hace que el registro pase por la CDN de Cloudflare — así ni siquiera el registro DNS revela la IP real.

Túneles Cloudflare

Los VPS también usan Cloudflare Tunnels (antes Argo Tunnel) como vía de entrada alternativa. Un túnel crea una conexión saliente desde el servidor hasta el edge de Cloudflare — sin puertos entrantes. Algunos servicios VPS usan túneles en vez de exponer puertos directamente.

Capa 4: reverse proxy con Caddy

Caddy es el servidor web que se encarga de la terminación TLS y el enrutamiento de peticiones. Hay dos instancias de Caddy con roles diferentes.

von-braun (borde de internet)

El Caddy de von-braun es el punto de entrada principal para el tráfico público de internet hacia el homelab. Corre como una imagen Docker personalizada con tres plugins:

FROM caddy:2.10-builder AS builder
RUN xcaddy build \
    --with github.com/caddy-dns/cloudflare \
    --with github.com/hslatman/caddy-crowdsec-bouncer/http \
    --with github.com/mholt/caddy-ratelimit

FROM caddy:2.10.0
COPY --from=builder /usr/bin/caddy /usr/bin/caddy

caddy-dns/cloudflare obtiene certificados TLS vía DNS challenge de Cloudflare (sin necesidad de exponer el puerto 80 para el HTTP challenge). caddy-crowdsec-bouncer se integra con CrowdSec para bloquear IPs maliciosas. caddy-ratelimit limita peticiones por IP de origen.

El Caddyfile usa snippets reutilizables para mantener una seguridad consistente en todos los sitios:

(web_security) {
    crowdsec
    rate_limit {
        zone per_ip {
            key {remote_host}
            events 1000
            window 1m
        }
    }
    encode zstd gzip
    import security_headers
}

zetesis.xyz {
    import cf_tls
    import web_security
    reverse_proxy http://zetesis-proxy:80
}

auth.zetesis.xyz {
    import cf_tls
    import auth_security
    reverse_proxy http://zetesis-proxy:80
}

El upstream zetesis-proxy es un proxy Tailscale que reenvía tráfico a Traefik dentro del clúster cortes. El recorrido completo del tráfico:

Usuario -> Cloudflare CDN -> Caddy (von-braun) -> Tailscale -> Traefik (cortes) -> Pod

CrowdSec corre como contenedor sidecar, leyendo los logs de acceso de Caddy y comparando patrones de tráfico contra inteligencia de amenazas mantenida por la comunidad. Si detecta un ataque (fuerza bruta, intentos de explotar CVEs, credential stuffing), le dice a Caddy que bloquee la IP.

colon (red local)

La segunda instancia de Caddy, colon, corre en la VM aristoteles de la red Roma. Su trabajo es interno: sirve los subdominios *.zetesis.localhost para servicios locales — interfaces web de Proxmox, TrueNAS, dashboards de los routers, la interfaz de Zigbee2MQTT y demás:

# Reverse proxy interno para servicios locales
kepler.zetesis.localhost {
    import cf_tls
    reverse_proxy http://10.0.0.7:3000
}

spinoza.zetesis.localhost {
    import cf_tls
    reverse_proxy 10.1.0.11:443 {
        transport http { tls; tls_insecure_skip_verify }
    }
}

rothbard.zetesis.localhost {
    import cf_tls
    reverse_proxy 10.1.0.7:8082
}

colon puede alcanzar servicios en ambas redes (10.0.0.x y 10.1.0.x) gracias al túnel WireGuard entre los routers. Usa DNS challenge de Cloudflare para TLS, así que todos los servicios internos tienen HTTPS aunque no estén expuestos a internet.

El recorrido completo

Tracemos una petición de principio a fin. Un usuario visita zetesis.xyz:

Loading diagram...
  1. El navegador resuelve zetesis.xyz a la IP de Cloudflare (no la nuestra)
  2. Cloudflare termina TLS, aplica sus protecciones y reenvía la petición a von-braun
  3. Caddy en von-braun consulta CrowdSec y aplica rate limits
  4. Caddy reenvía por Tailscale al clúster cortes
  5. Traefik (el controlador ingress de Kubernetes) enruta por Host header al pod correcto
  6. La respuesta viaja de vuelta por el mismo camino

En ningún momento queda expuesta una IP doméstica ni un puerto interno. Los únicos servidores accesibles públicamente son los VPS y el edge de Cloudflare.

Compáralo con una petición interna — acceder al dashboard de Proxmox desde un portátil en la red Roma:

Portátil (10.0.0.x) -> Caddy/colon (10.0.0.7) -> pitagoras Proxmox (10.0.0.3:8006)

Dos saltos, todo en la red local. Y para acceso entre sitios:

Portátil (10.0.0.x) -> router constantino -> túnel WireGuard -> router temistocles -> spinoza (10.1.0.11)

El túnel WireGuard es transparente — el portátil no sabe que está cruzando un túnel. Simplemente enruta hacia 10.1.0.11 y los routers se encargan del resto.

¿Por qué sin port forwarding?

Los homelabs tradicionales abren puertos en el router de casa: el puerto 443 va a un reverse proxy, el 51820 a WireGuard, etc. Yo lo evito por completo por tres motivos.

Seguridad. Un puerto abierto es una puerta abierta. Aunque tengas firewall, confías en que cada servicio detrás de ese puerto sea seguro. Con Tailscale no hay conexiones entrantes — todo es saliente.

CGNAT. Algunos ISP usan Carrier-Grade NAT, lo que significa que compartes IP pública con otros clientes y no puedes abrir puertos aunque quieras. Tailscale y los túneles de Cloudflare funcionan a través de CGNAT porque solo hacen conexiones salientes.

Sencillez. Sin reglas de port forwarding que mantener, sin NAT reflection que configurar, sin preocuparte de que un cambio de IP rompa algo.

La única excepción es el puerto 51820 de WireGuard entre los dos routers — pero eso es específicamente para el túnel sitio a sitio, no para servicios de cara al público.

Recomendaciones

Si estás montando un homelab, esta es la pila de red que recomiendo:

WireGuard para túneles sitio a sitio entre ubicaciones físicas. Es rápido, sencillo, y viene integrado en la mayoría de sistemas operativos de router. Tailscale para conectar servidores en la nube y dispositivos remotos. La capa gratuita es generosa, las ACL son potentes, y nunca necesitas abrir un puerto. Cloudflare para DNS, protección DDoS y ocultar tu IP real. La capa gratuita cubre todo lo que necesita un homelab. Caddy como reverse proxy con TLS automático. Más sencillo que Nginx, buen ecosistema de plugins, y el formato del Caddyfile se entiende con leerlo.

El principio más importante: nunca expongas tu IP doméstica ni tus servicios internos directamente a internet. Usa Tailscale y Cloudflare como escudo. El tráfico entra por Cloudflare a un VPS, y el VPS alcanza el homelab a través de Tailscale. Tu red de casa queda invisible.

En el próximo post veremos lo que corre sobre esta red: Kubernetes con Talos Linux, GitOps con ArgoCD, y el sistema de CD ligero (ptolomeo) que mantiene los despliegues Docker Compose sincronizados.


Siguiente: Post 3 - Kubernetes y GitOps | Anterior: Post 1 - La visión general