Problema
En un homelab o cualquier entorno self‑hosted es frecuente ejecutar varios servicios detrás de un proxy inverso. Cada servicio necesita TLS para evitar tráfico en texto plano, pero la validación HTTP‑01 de Let’s Encrypt no funciona cuando el objetivo está aislado en la red local o no es accesible desde Internet. El reto es decidir entre certificados auto‑firmados y certificados públicos obtenidos mediante el desafío DNS‑01, y además garantizar que la renovación y rotación sean automáticas y seguras.
Causa
- Falta de exposición pública – Los contenedores solo escuchan en la LAN; los servidores de validación de Let’s Encrypt no pueden alcanzar
http://host/.well-known/acme-challenge/…. - Limitaciones de los routers – Redirecciones de puertos o NAT pueden estar deshabilitadas por motivos de seguridad o por falta de IP estática.
- Gestión manual de certificados – Al crear certificados a mano, la renovación pasa desapercibida y termina en expiraciones inesperadas.
- Almacenamiento de credenciales DNS – El desafío DNS‑01 requiere acceso a la API del proveedor de DNS; si esas credenciales se guardan sin cifrar, el proceso se vuelve un vector de ataque.
Solución
Adoptar un flujo de trabajo basado en DNS‑01 con Let’s Encrypt para los dominios que deben ser accesibles desde fuera (p. ej., example.com) y certificados auto‑firmados únicamente para servicios estrictamente internos. El proceso se divide en tres partes:
1. Configurar un gestor de certificados que soporte DNS‑01
Herramientas como certbot (con plugins DNS) o lego (CLI ligera) pueden crear y renovar certificados usando la API del proveedor DNS. La elección depende del ecosistema: si ya usas Docker Compose, certbot con un contenedor oficial simplifica la integración; lego es útil cuando se prefiere una binaria estática.
Ejemplo con certbot + Cloudflare
docker run -it --rm \
-v "$(pwd)/certs:/etc/letsencrypt" \
-v "$(pwd)/cloudflare.ini:/cloudflare.ini:ro" \
certbot/dns-cloudflare \
certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /cloudflare.ini \
-d example.com -d *.example.com \
--non-interactive --agree-tos --email admin@example.com
cloudflare.ini contiene:
dns_cloudflare_email = your@email.com
dns_cloudflare_api_key = 0123456789abcdef0123456789abcdef
El contenedor escribe los certificados en ./certs. Con un cron o un timer de systemd se ejecuta periódicamente (Let’s Encrypt permite 90‑día de validez, pero se renueva cada 60 días).
2. Inyectar los certificados en el proxy inverso
Traefik y Caddy son los proxies más usados en Docker Compose porque pueden leer los certificados desde un volumen compartido y recargar automáticamente. Un fragmento de docker‑compose.yml con Traefik:
services:
traefik:
image: traefik:v3.0
command:
- "--providers.file.directory=/certs"
- "--entrypoints.websecure.http.tls=true"
ports:
- "80:80"
- "443:443"
volumes:
- "./certs:/certs:ro"
- "/var/run/docker.sock:/var/run/docker.sock:ro"
Cada contenedor que exponga un servicio declara la etiqueta traefik.http.routers.<name>.tls.certresolver=default y Traefik usará los archivos fullchain.pem y privkey.pem que certbot generó.
3. Usar certificados auto‑firmados para la LAN
Para servicios que nunca se exponen fuera de la red (por ejemplo, una instancia de Home Assistant accesible solo vía VPN), generar un CA interno y firmar los certificados evita la sobrecarga de DNS‑01. El proceso se hace una sola vez:
# Crear CA
openssl genrsa -out ca.key 4096
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.crt -subj "/CN=Homelab CA"
# Firmar un certificado para internal.example.local
openssl genrsa -out internal.key 2048
openssl req -new -key internal.key -out internal.csr -subj "/CN=internal.example.local"
openssl x509 -req -in internal.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out internal.crt -days 825 -sha256
Distribuir ca.crt a todos los clientes (navegadores, dispositivos móviles) elimina la advertencia de “certificado no confiable”. En Docker Compose, monta ca.crt como archivo de confianza dentro del contenedor o usa la variable de entorno SSL_CERT_FILE.
Cuándo aplicar esta solución
| Señal | Acción recomendada |
|---|---|
| El dominio tiene un registro DNS público y necesita acceso externo | Usa DNS‑01 con Let’s Encrypt (certbot o lego). |
| El servicio está aislado a la LAN y no tiene registro DNS público | Genera certificados auto‑firmados con una CA interna. |
| Necesitas renovación automática sin intervención humana | Programa el contenedor de certbot en cron (ej. 0 2 * * *). |
| El proveedor DNS no tiene plugin oficial | Usa lego con la opción --dns <provider> o scripts hook personalizados. |
| No quieres almacenar credenciales DNS en texto plano | Emplea Docker secrets o HashiCorp Vault y pasa la variable al contenedor en tiempo de ejecución. |
No apliques DNS‑01 si el dominio es interno y no tiene registro público; la validación fallará y consumirás cuota de la API sin necesidad.
Código
# Script de renovación automática (cron)
0 3 * * * docker run --rm \
-v "$(pwd)/certs:/etc/letsencrypt" \
-v "$(pwd)/cloudflare.ini:/cloudflare.ini:ro" \
certbot/dns-cloudflare renew \
--dns-cloudflare-credentials /cloudflare.ini \
--quiet && docker kill -s HUP $(docker ps -q -f name=traefik)
El HUP fuerza a Traefik a recargar los certificados sin reiniciar el contenedor.
## Verificación
1. **Comprobar fechas**
```bash
openssl x509 -noout -dates -in certs/live/example.com/fullchain.pem
Verifica que notAfter esté a más de 30 días en el futuro.
-
Validar cadena de confianza
openssl verify -CAfile certs/live/example.com/chain.pem certs/live/example.com/fullchain.pemSalida
OKindica que la cadena está completa. -
Probar el proxy
curl -v https://example.com --cacert certs/live/example.com/chain.pemEl código de estado 200 y ausencia de errores TLS confirman que el certificado está activo.
Notas adicionales
- Rate limits: Let’s Encrypt permite 50 certificados por dominio registrado por semana. Usa
--reuse-keycuando renueves para no consumir cuotas adicionales. - Propagación DNS: Algunos proveedores tardan varios minutos en crear el registro TXT necesario para DNS‑01. Configura
--dns-cloudflare-propagation-seconds 60(o el equivalente) para evitar fallos de validación. - Seguridad de credenciales: Nunca incluyas el archivo
.inien el repositorio. Usa.dockerignorey, si es posible, monta el secreto desde un gestor externo. - Rotación de CA interna: Cuando la CA auto‑firmada se acerca a su expiración, genera una nueva y actualiza los clientes antes de revocar la anterior para evitar interrupciones.
- Compatibilidad con Kubernetes: El mismo enfoque funciona con cert‑manager; simplemente crea un
ClusterIssuercondns01y apunta a la misma API DNS.
Con este flujo, los servicios en un homelab pueden disponer de TLS fiable tanto para acceso externo como interno, sin depender de validaciones HTTP‑01 imposibles y sin caer en la trampa de los certificados caducados.