Problema

Muchas instalaciones de homelab o entornos de producción ligera necesitan certificados TLS para dominios y sub‑dominios, a menudo con soporte wildcard. El método DNS‑01 de ACME es el único que permite wildcard, pero requiere que el cliente pueda crear y borrar registros TXT en la zona DNS. La práctica más extendida es dar a Certbot (o a un proxy como Traefik) acceso mediante una API del proveedor DNS. Eso implica almacenar una clave API en el host, lo que genera dos problemas recurrentes:

  1. Superficie de ataque ampliada – cualquier proceso con acceso a la clave puede modificar la zona completa.
  2. Mantenimiento frágil – los contenedores que gestionan la API y el cliente suelen estar acoplados; una actualización rompe la comunicación o los volúmenes se desincronizan.

El patrón que se repite es “necesito DNS‑01 sin exponer credenciales y con la mínima cantidad de componentes”. La solución debe ser portable, fácil de desplegar en Docker (o en una VM) y capaz de delegar únicamente el sub‑dominio _acme-challenge mediante un registro CNAME.

Causa

Los fallos habituales provienen de tres áreas:

  • Dependencia de API del registrador – la mayoría de los proveedores solo exponen una API REST; si el contenedor que la consume se reinicia, la clave puede quedar expuesta en logs o volúmenes temporales.
  • Arquitectura multi‑container – separar acme‑dns, la UI y Certbot en contenedores obliga a crear redes internas, montar volúmenes compartidos y exponer puertos internos. Cada capa añade latencia y puntos de falla.
  • Gestión de puerto 53 – el servicio DNS necesita escuchar en el puerto 53/UDP (y a veces TCP). En entornos donde otro DNS local ya usa ese puerto, el despliegue colisiona y el contenedor no arranca.

Solución

Una arquitectura monolítica dentro de un único contenedor elimina la mayor parte de la complejidad. El contenedor ejecuta:

  1. acme‑dns escuchando en 0.0.0.0:53/udp (y TCP para consultas de zona).
  2. API HTTP que permite registrar dominios y generar los registros TXT necesarios.
  3. UI ligera (por ejemplo Nuxt) para crear cuentas y visualizar los valores de desafío.
  4. Certbot (o cualquier cliente ACME) configurado para usar el endpoint HTTP de acme‑dns como “authenticator”.

Paso a paso general

  1. Crear zona delegada
    En el panel del registrador crea un registro CNAME llamado _acme-challenge.tu-dominio.com que apunte a acme-dns.tu-dominio.com. acme-dns.tu-dominio.com será la zona gestionada por el contenedor.

  2. Construir la imagen
    Usa un Dockerfile que combine acme-dns, certbot y la UI. La mayoría de los proyectos open‑source ya publican una imagen lista; si prefieres compilar, parte de golang:alpine para acme‑dns y añade certbot desde python:alpine.

  3. Ejecutar el contenedor

    • Mapear el puerto 53 tanto UDP como TCP.
    • Montar un volumen persistente para la base SQLite de acme‑dns y para los certificados de Certbot.
    • Definir la variable ACME_DNS_API_URL para que Certbot apunte al endpoint interno (http://localhost:8080/acme-dns).
  4. Registrar la zona en acme‑dns
    La UI o la API permite crear una “account” y asignarle la zona delegada. El cliente ACME recibirá automáticamente los valores TXT que acme‑dns sirve.

  5. Solicitar / renovar certificados
    Ejecuta Certbot con el plugin --manual apuntando al endpoint HTTP de acme‑dns. La renovación puede programarse con cron dentro del mismo contenedor.

Ventajas de la solución única

  • Sin claves API externas – solo el registro CNAME es necesario en el registrador.
  • Persistencia simple – un único volumen contiene tanto la base de datos de desafíos como los certificados.
  • Actualizaciones atómicas – al actualizar la imagen, todo el stack se reinicia de forma coherente.
  • Portabilidad – funciona en cualquier host Docker, incluso en Synology, Raspberry Pi o una VM en la nube.

Cuándo aplicar esta solución

Escenarios ideales

  • Homelabs o servidores personales que gestionan dominios propios y necesitan wildcard.
  • Entornos donde el registrador no ofrece una API segura o la política de la empresa prohíbe almacenar tokens.
  • Deployments con recursos limitados que prefieren un solo contenedor en lugar de varios micro‑servicios.

Señales de que la solución encaja

  • Necesitas renovar certificados automáticamente sin intervención manual.
  • El puerto 53 está libre o puedes redirigir tráfico DNS a la máquina que ejecuta el contenedor.
  • Quieres evitar que el proceso de reverse proxy (Traefik, Caddy) tenga permisos de escritura en la zona DNS.

Casos donde no aplica

  • Infraestructuras que usan DNS interno con políticas de zona estrictas y no permiten delegar sub‑zonas.
  • Entornos que requieren alta disponibilidad de DNS y no pueden depender de un solo contenedor para responder consultas.
  • Situaciones donde el registrador exige autenticación basada en API para cada desafío (p.ej., Cloudflare con rate‑limit estricto).

Código

docker run -d \
  --name dns01-stack \
  --restart unless-stopped \
  -p 53:53/udp -p 53:53/tcp \
  -v /srv/dns01/data:/data \
  -e ACME_DNS_API_URL=http://localhost:8080/acme-dns \
  ghcr.io/usuario/dns01-stack:latest
  • /srv/dns01/data contendrá acme-dns.db y los certificados de Certbot.
  • La variable ACME_DNS_API_URL solo es necesaria si la imagen no asume localhost.

Para solicitar un certificado wildcard:

docker exec dns01-stack certbot certonly \
  --manual \
  --preferred-challenges dns \
  --manual-auth-hook "/usr/local/bin/acme-dns-auth.sh" \
  --manual-cleanup-hook "/usr/local/bin/acme-dns-cleanup.sh" \
  -d "*.example.com" -d "example.com"

Los scripts acme-dns-auth.sh y acme-dns-cleanup.sh están preinstalados en la imagen y utilizan la API interna.


## Verificación
1. **Comprobar que acme‑dns responde**  
   ```bash
   dig @127.0.0.1 _acme-challenge.example.com TXT +short

Debería devolver el valor generado por la API.

  1. Validar el certificado

    openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | openssl x509 -noout -dates
    

    La fecha de expiración debe coincidir con la renovada por Certbot.

  2. Revisar logs del contenedor

    docker logs dns01-stack --tail 20
    

    Busca líneas que indiquen “challenge completed” y “certificate saved”.

  3. Simular una renovación
    Forzar la renovación con certbot renew --dry-run dentro del contenedor y confirmar que el proceso completa sin errores.

Notas adicionales

  • Firewall – Asegúrate de que el host permite tráfico UDP/TCP en el puerto 53 desde la red donde se ejecuta Let’s Encrypt (generalmente internet). Un iptables -A INPUT -p udp --dport 53 -j ACCEPT suele ser suficiente.
  • TTL bajo – Configura un TTL corto (5‑10 min) para el registro CNAME delegante; esto reduce el tiempo de propagación cuando cambias de contenedor o actualizas la base de datos.
  • Backup de la base SQLite – Copia periódicamente /srv/dns01/data/acme-dns.db. Sin él, perderás la asociación entre dominios y tokens, obligando a recrear los registros CNAME.
  • Escalado – Si en el futuro necesitas alta disponibilidad, puedes replicar la base SQLite mediante un NFS compartido y lanzar varios contenedores detrás de un balanceador de carga que distribuya consultas DNS (aunque la especificación de ACME espera que el desafío sea servido por un único servidor).

Con este enfoque, obtienes certificados wildcard sin exponer claves API, mantienes la infraestructura simple y reduces la superficie de fallo a un único contenedor Docker gestionable.