Problema
En muchos homelabs los servicios internos (Forgejo, Home Assistant, etc.) necesitan HTTPS para evitar advertencias del navegador y para que otras aplicaciones confíen en ellos. El obstáculo típico es que el dominio público está asociado a una IP pública, mientras que los servicios residen en redes privadas (192.168.x.x). Cuando se crea un registro DNS que apunta directamente a la IP interna, los resolvers externos no pueden resolverlo y los clientes locales obtienen “Server not found”. Además, Let’s Encrypt rechaza la emisión de certificados si el dominio no es accesible públicamente o si la validación DNS‑01 falla por una configuración incorrecta del servidor DNS interno.
El patrón recurrente es: un dominio registrado en un proveedor externo, un servidor DNS interno (Adguard Home, Pi‑hole) que debería resolver ese dominio a una IP privada, y un reverse proxy (Caddy) que necesita obtener certificados automáticamente. Cuando cualquiera de esos componentes no está alineado, la cadena de resolución se rompe y el certificado nunca se emite.
Causa
-
Resolución DNS fragmentada
- Los clientes usan resolvers externos (ISP) antes de llegar a Adguard Home, por lo que el registro interno nunca se consulta.
- En redes con split‑DNS, la zona no está delegada al servidor interno, lo que genera “NXDOMAIN”.
-
Validación de Let’s Encrypt sin acceso público
- El método HTTP‑01 requiere que la autoridad de certificación (CA) alcance el servidor a través de la IP pública. Si el puerto 80/443 está bloqueado o redirigido a otro host, la validación falla.
- La alternativa DNS‑01 necesita credenciales API del proveedor DNS (porkbun) y una zona pública que incluya el registro
_acme-challenge. Si el registro se crea solo en el DNS interno, la CA no lo ve.
-
Caddy configurado para usar DNS‑01 pero sin delegar la zona
- La directiva
dns porkbunsolo funciona cuando el dominio está gestionado por Porkbun y la zona pública contiene los registros de desafío. Si el registro interno sobrescribe la zona pública (por ejemplo, mediante “DNS rewrite” en Adguard), la CA recibe una respuesta vacía.
- La directiva
-
Firewalls o NAT mal configurados
- Incluso con la zona pública correcta, la petición de validación puede quedar atrapada en el router si el puerto 443 no se reenvía al contenedor Caddy.
Solución
1. Definir una estrategia de validación
- Preferir DNS‑01 cuando el dominio está gestionado por un proveedor que ofrece API (porkbun, Cloudflare, etc.).
- Usar HTTP‑01 solo si el servicio está expuesto públicamente (por ejemplo, mediante un subdominio dedicado que sí apunta a la IP pública y redirige al reverse proxy).
2. Configurar split‑DNS correctamente
-
Crear una zona pública en Porkbun con los registros A/CNAME que apunten a la IP pública del router.
-
Añadir los registros
_acme-challengemediante la API de Porkbun; Caddy lo hará automáticamente. -
En Adguard Home (o Pi‑hole) crear una zona “override” para el mismo dominio, pero solo para los hosts internos (por ejemplo,
forgejo.domain.tld → 192.168.1.101). La zona interna no debe contener_acme-challenge; deja que la consulta caiga en la zona pública cuando la CA la solicite.- En Adguard Home: Settings → DNS → Overrides → Add override → Marca “Never forward private IPs” desactivado y escribe la IP interna.
-
Asegurarse de que los clientes usen Adguard como DNS primario. En el router, establecer la IP de Adguard como DNS DHCP. Los dispositivos que usan DNS externo seguirán sin resolver el dominio interno, lo cual es deseable para evitar fugas.
3. Configurar Caddy para DNS‑01 con Porkbun
# Caddyfile (fragmento esencial)
{
email [email protected]
acme_dns porkbun {
api_key "TU_API_KEY"
api_secret_key "TU_API_SECRET"
}
}
# Servicio interno
forgejo.domain.tld {
reverse_proxy 192.168.1.101:443 {
transport http {
tls
tls_insecure_skip_verify
}
}
}
- La directiva global
{}define el resolver DNS‑01. - Cada host interno se declara con su nombre público; Caddy solicitará el certificado a Let’s Encrypt usando la API de Porkbun.
- El bloque
reverse_proxyapunta a la IP privada; la opcióntls_insecure_skip_verifyevita que Caddy valide el certificado del backend (el backend puede usar un certificado autofirmado).
4. Abrir puertos y NAT
- En el router, redirigir puerto 443 (y opcionalmente 80) a la IP del contenedor Caddy.
- Desactivar cualquier regla que reescriba el tráfico a la UI del router.
- Verificar que la IP pública responde a
https://forgejo.domain.tld/.well-known/acme-challenge/...con un código 200 (puede hacerse concurl -k https://forgejo.domain.tld/.well-known/acme-challenge/test).
5. Automatizar renovación
Caddy maneja la renovación automáticamente siempre que la zona DNS pública siga siendo válida. No es necesario programar cron; sin embargo, es buena práctica monitorear los logs de Caddy (docker logs caddy) para detectar errores de renovación.
Cuándo aplicar esta solución
- Entornos homelab o pequeñas oficinas con dominios gestionados por un proveedor que expone API (porkbun, Cloudflare, Namecheap).
- Servicios internos que requieren HTTPS y que no pueden exponerse directamente a Internet.
- Redes con split‑DNS donde los dispositivos internos usan un resolver propio (Adguard, Pi‑hole).
No es adecuada si:
- El proveedor DNS no permite actualizaciones vía API.
- No se dispone de una IP pública o el router no permite NAT de puertos 80/443.
- Se prefiere una solución totalmente offline (por ejemplo, usar solo certificados autofirmados y confiar en ellos manualmente).
Código
# Instalar el plugin DNS para Porkbun en Caddy (si usas la versión oficial)
docker run -d \
--name caddy \
-p 80:80 -p 443:443 \
-v $(pwd)/Caddyfile:/etc/caddy/Caddyfile \
-v caddy_data:/data \
-v caddy_config:/config \
caddy:latest \
caddy run --config /etc/caddy/Caddyfile --adapter caddyfile
Verificación
-
Resolución DNS interna
dig @ADGUARD_IP forgejo.domain.tld +short # debe devolver 192.168.1.101 -
Resolución DNS pública (desde una máquina fuera de la red)
dig @8.8.8.8 forgejo.domain.tld +short # debe devolver la IP pública del router -
Comprobar certificado
openssl s_client -connect forgejo.domain.tld:443 -servername forgejo.domain.tld </dev/null 2>/dev/null | openssl x509 -noout -dates # las fechas deben estar dentro del rango de validez de Let's Encrypt -
Revisión de logs
docker logs caddy 2>&1 | grep "obtaining certificate" # debe mostrar "certificate obtained" sin errores -
Acceso web
Abrirhttps://forgejo.domain.tlden el navegador interno; el candado verde indica certificado válido y la página carga sin redirección al router.
Notas adicionales
- TTL bajo: Configura TTL de 300 s para los registros A internos; facilita cambios rápidos cuando reubicas contenedores.
- Separar subdominios: Usa
*.internal.domain.tldpara todos los servicios internos y reservadomain.tldpara la zona pública. - Fallback DNS: Mantén Pi‑hole como segundo resolver; si Adguard falla, los clientes seguirán resolviendo la zona pública (aunque sin acceso interno).
- Logs de Adguard: Si la resolución interna no funciona, revisa
AdguardHome.logpara detectar “refused” o “NXDOMAIN”. - Actualizaciones de Caddy: Las versiones mayores pueden cambiar la sintaxis del plugin DNS; revisa la documentación oficial antes de actualizar.