---
title: "Reverse proxy en el homelab: Caddy, TLS y cuando Pi-hole se interpone"
description: "Caddy como reverse proxy en el homelab: TLS, split DNS, rebind protection y renovaciones ACME con Pi-hole, después de VPN y sslip.io."
date: 2026-08-15
locale: es
url: https://escribano.dev/es/blog/008-caddy-reverse-proxy-pihole/
---
En el [post sobre sslip.io](/blog/007-sslip-io-homelab-dns/) la capa de nombres quedó resuelta de forma mínima: hostnames que apuntan a una IP sin comprar dominio ni montar BIND. Pero un nombre no sirve de mucho si cada servicio sigue escuchando en su puerto (`:3000`, `:8123`, `:9000`) y el navegador sigue gritando por certificados autofirmados.

La siguiente capa es terminar TLS y enrutar HTTP: un reverse proxy que recibe en el 443, presenta un certificado válido y reparte el tráfico según el hostname. Caddy, nginx y Traefik hacen eso; este post se centra en Caddy porque encaja con el perfil del homelab personal (TLS automático, configuración legible, footprint modesto) y porque es donde aparecen las fricciones reales al mezclarlo con un resolver local como Pi-hole.

El hilo es el marco: qué problema resuelve el proxy, qué rompe cuando añades DNS interno, y cómo encaja en el [principio RPS](/blog/002-rps-principle/).

## La pila hasta aquí

Recuerda el orden que venimos construyendo:

1. **Acceso a la red**: [Tailscale, WireGuard u OpenVPN](/blog/006-homelab-vpn-access/)
2. **Resolución de nombres**: [sslip.io como puente](/blog/007-sslip-io-homelab-dns/), MagicDNS, o Pi-hole
3. **Terminación TLS y enrutado HTTP** ← aquí
4. **Identidad de aplicación**: OIDC, forward auth; [Zitadel y el espectro RPS](/blog/009-homelab-identity/)

El proxy no sustituye la VPN ni el DNS. Asume que ya sabes llegar a la red y resolver el nombre. Su trabajo es que `https://grafana.192-168-1-47.sslip.io` llegue al contenedor correcto con un certificado que el navegador acepte.

## Qué hace un reverse proxy (y qué no)

Un reverse proxy escucha en los puertos estándar (80 y 443) y reenvía el tráfico a servicios internos según reglas: hostname, path, headers. En el homelab eso traduce:

| Sin proxy | Con proxy |
|-----------|-----------|
| `192.168.1.47:3000` | `https://grafana.lan` |
| Certificado por servicio (o ninguno) | Un certificado en el borde |
| Recordar puertos | Recordar nombres |

Lo que no hace: autenticar usuarios de aplicación (eso es la capa 4) ni resolver nombres DNS (eso es la capa 2). Confundir capas lleva a montar OAuth en Caddy cuando el problema era solo enrutar, o a exponer servicios sin perímetro cuando aún no tienes VPN.

## Caddy en el marco RPS

| Solución | RPS | Qué pagas | Qué ganas |
|----------|-----|-----------|-----------|
| Acceso directo por puerto | Piedra mínima | Superficie expuesta, sin TLS unificado | Cero proxy |
| **Caddy** | Piedra operativa | Aprender su DSL; acoplamiento al ecosistema Caddy | TLS automático, config declarativa, bajo mantenimiento |
| **nginx** | Piedra / papel | Más config manual; Certbot aparte | Máximo control, referencia universal |
| **Traefik** | Piedra operativa | Etiquetas Docker, modelo mental de routers | Descubrimiento automático en stacks containerizados |
| **Cloudflare Tunnel** | Piedra operativa | Ancla en Cloudflare | Sin abrir puertos; TLS gestionado fuera |

Caddy es piedra operativa en el sentido bueno: resuelve TLS + routing con poca ceremonia. El coste es una ancla blanda en el ecosistema (migrar a nginx no es imposible, pero reescribes configs) y la tentación de usar features propietarias de Caddy (`forward_auth`, módulos) sin evaluar reversibilidad.

Para un homelab de una persona que ya eligió simplicidad operativa en la capa VPN, Caddy es la continuación honesta.

## TLS: HTTP-01, sslip.io y la LAN privada

En el post anterior vimos que Let's Encrypt con [HTTP-01](https://letsencrypt.org/docs/challenge-types/#http-01-challenge) funciona con `203-0-113-7.sslip.io` si la IP pública es alcanzable en el puerto 80. Caddy lo automatiza: declaras el hostname, Caddy obtiene y renueva el certificado.

Con IPs privadas (`192.168.x.x`) la cosa cambia. Let's Encrypt no puede llegar a tu LAN desde internet. Opciones reales:

* Certificado autofirmado: rápido, el navegador protesta.
* CA interna: más trabajo, viable en red controlada.
* DNS-01 con wildcard: tijeras; sslip.io lo documenta, no es el camino por defecto.
* Estar dentro de la VPN/tailnet con TLS gestionado por Tailscale, ya cubierto en el [post de acceso](/blog/006-homelab-vpn-access/).

Caddy automatiza el baile cuando las condiciones se cumplen; la física de la red sigue ahí.

### Subdominios con sslip.io

Un patrón habitual: `grafana.192-168-1-47.sslip.io` apunta a la misma IP que `192-168-1-47.sslip.io`; Caddy enruta por hostname al backend correcto. Eso permite varios servicios detrás de un solo proxy sin Pi-hole ni dominio propio, mientras la IP no cambie y aceptes la estética del nombre.

### Ejemplo mínimo de `Caddyfile`

Un bloque por hostname ilustra el patrón: TLS automático, backend en la LAN.

```caddyfile
{
	# Opcional: email para avisos de Let's Encrypt
	email admin@example.com
}

grafana.192-168-1-47.sslip.io {
	reverse_proxy 192.168.1.47:3000
}

homeassistant.192-168-1-47.sslip.io {
	reverse_proxy 192.168.1.48:8123
}
```

Caddy escucha en 80 y 443, obtiene certificados para cada hostname y reenvía al puerto interno. Si la IP del proxy es `192.168.1.47` y los backends viven en otras máquinas, cambia las IPs de `reverse_proxy`: el hostname sslip.io sigue apuntando a la IP del proxy, no a la del backend.

Con IP privada y sin salida pública al 80, añade `tls internal` dentro del bloque para forzar certificado autofirmado de Caddy (útil en LAN, inútil para visitantes desde internet).

## Pi-hole entra en escena

sslip.io resuelve nombres sin infraestructura propia. Pi-hole (con Unbound detrás) es el salto a DNS soberano en la LAN: registros arbitrarios (`grafana.homelab`, `nas.lan`), bloqueo de ads, control total del resolver.

El problema: mezclar sslip.io con Pi-hole sin criterio es falsa piedra. Dos resolvers, reglas contradictorias, y dolores de cabeza que solo aparecen cuando montas el proxy.

### Split DNS

Split DNS significa que la misma pregunta DNS tiene respuesta distinta según quién pregunta:

| Consulta | Desde internet | Desde la LAN (Pi-hole) |
|----------|----------------|------------------------|
| `app.midominio.com` | IP pública del router | IP interna del proxy (`192.168.1.47`) |

Sin split DNS, un dispositivo en casa puede resolver tu dominio a la IP pública, salir al router, hacer hairpin NAT y fallar, o ir por un camino lento e impredecible. Con split DNS, la LAN resuelve directo a la IP interna.

Caddy necesita que el cliente y el proxy estén de acuerdo en qué hostname usa cada servicio. Si Pi-hole devuelve una cosa y el resolver público otra, el proxy emite certificados para un nombre que desde dentro no resuelve igual.

### Rebind protection

Pi-hole incluye protección contra [DNS rebinding](https://en.wikipedia.org/wiki/DNS_rebinding): bloquea respuestas que apuntan a IPs privadas para dominios públicos. Es una defensa legítima.

Pero sslip.io devuelve IPs privadas por diseño (`192-168-1-47.sslip.io` → `192.168.1.47`). Pi-hole puede bloquear esas respuestas. Síntoma: el nombre resuelve fuera de casa y falla dentro, o al revés.

Solución: whitelist de dominios de confianza (`sslip.io`, `nip.io`) o desactivar rebind protection para esos casos, consciente del trade-off de seguridad.

### Renovaciones ACME con Pi-hole

Cuando Caddy renueva un certificado con HTTP-01, Let's Encrypt consulta DNS público y conecta a tu IP pública en el puerto 80. Si Pi-hole intercepta todas las consultas DNS (incluidas las de la propia máquina del proxy) y devuelve la IP interna para tu dominio, la renovación puede fallar: LE intenta llegar a `192.168.x.x` desde internet.

Patrones que funcionan:

* **Local DNS records** en Pi-hole para el dominio público apuntando a la IP interna solo para clientes LAN, más regla de firewall que permita al proxy responder en el 80 desde fuera.
* **DNS condicional**: el proxy usa resolver upstream distinto al de los clientes para las consultas ACME.
* **DNS-01** en lugar de HTTP-01 (más complejo, evita el problema del hairpin).

No hay una receta única; depende de si tienes IP pública, CG-NAT, o solo acceso por tailnet. Antes de culpar a Caddy, diagnostica con `dig` (@Pi-hole vs @8.8.8.8).

## Orden de arranque y dependencias

Otra fricción silenciosa: Caddy arranca y quiere renovar certificados; Pi-hole aún no está listo; el proxy resuelve mal y entra en bucle de reintentos. O al revés: Pi-hole depende de un contenedor que Caddy aún no enruta.

Reglas prácticas:

* Pi-hole y el proxy en hosts estables (IP fija o DHCP reservation).
* `depends_on` en Docker no garantiza que el servicio esté *listo*; healthchecks si la pila es frágil.
* Documentar qué servicio debe arrancar primero en un apagado largo, no confiar en la memoria.

## En el marco RPS: puentes y anclas

| Decisión | Tipo | Por qué |
|----------|------|---------|
| Caddy con `Caddyfile` versionado | Puente | Migrar a nginx es trabajo, no rehacer la topología |
| Certificados Let's Encrypt en Caddy | Puente | Estándar; otro cliente ACME puede sustituirlo |
| Pi-hole como único resolver en la LAN | Piedra / ancla | Control y fricción operativa a cambio de soberanía DNS |
| Mezclar sslip.io + Pi-hole + dominio propio sin mapa | Falsa piedra | Tres fuentes de verdad, debugging infernal |
| Cloudflare Tunnel como único borde | Ancla | TLS y routing delegados; difícil volver a self-hosted puro |

Conecta con [soberanía digital](/blog/005-digital-sovereignty/): el proxy puede estar en tu mini PC y los certificados en Let's Encrypt, pero si Pi-hole filtra mal una renovación, pierdes TLS y la cadena de confianza entera se resiente.

## Cuándo usar qué

Caddy + sslip.io basta cuando:

* Estás probando la pila antes de comprar dominio.
* Tienes IP pública y pocos servicios.
* Accedes sobre todo desde la tailnet con TLS de Tailscale para lo crítico.

Añade Pi-hole (o Unbound) cuando:

* Necesitas docenas de nombres locales arbitrarios (`*.homelab`).
* Quieres bloqueo de ads en toda la LAN.
* Tienes dominio propio y necesitas split DNS serio.

Pasa a dominio propio + Caddy cuando:

* El servicio es público o semi-profesional.
* sslip.io ya te parece frágil o feo para compartir.
* Necesitas estabilidad de nombre independiente de la IP.

## Limitaciones honestas

* Caddy no arregla CG-NAT. Sin IP pública alcanzable, HTTP-01 desde internet no funciona; necesitas tailnet, túnel, o DNS-01.
* Un proxy mal configurado es peor que ninguno. Superficie de ataque centralizada; un error en `Caddyfile` expone todo.
* Pi-hole + proxy + sslip.io + dominio real puede coexistir, pero exige un mapa explícito de qué resolver responde qué para quién.
* Let's Encrypt tiene límites de tasa. No pruebes renovaciones en bucle contra producción; usa staging o espera (cinco renovaciones fallidas por semana y empiezas a tener problemas).

## Lo que viene después

Con nombre, certificado y enrutamiento HTTP resueltos, la pregunta siguiente es quién puede hacer login: Grafana, tu PWA, el panel de administración. Ahí entra la capa de identidad (OIDC, forward auth, Zitadel frente a Authelia frente a "auth básica y listo") en el [post 009](/blog/009-homelab-identity/).

El proxy termina TLS; el DNS nombra; la VPN te mete en la red. Sin un mapa de qué resolver responde qué, las tres se pisan. Cada capa en su sitio, anclas conscientes.
