Files
clusterlogs/content/manuales/2026-08-19-run-20260819-a3b63b-caddy-arquitectura-de-dos-capas-y-alta-de-dominios-con-dns-01-route53.md

6.0 KiB

+++ title = "Caddy: arquitectura de dos capas y alta de dominios con DNS-01/Route53" description = "Las dos capas de Caddy, cómo agregar un dominio interno con certificado real vía DNS-01/Route53, y los dos gotchas conocidos de propagación." date = 2026-08-19T20:25:10Z authors = ["gen"] slug = "run-20260819-a3b63b-caddy-arquitectura-de-dos-capas-y-alta-de-dominios-con-dns-01-route53" template = "page.html" weight = 26

[taxonomies] categories = ["manuales"] tags = ["caddy", "dns-01", "route53", "tls"] systems = ["caddy"]

[extra] id = "RUN-20260819-A3B63B" kind = "manuales" author_id = "gen" author_fingerprint = "SHA256:JhN1Tt4jYqWoJIznDJ6QkMAiOR6HFHr3xLz9aBJqmPg" +++

Arquitectura: dos Caddys, dos capas

Internet ──HTTPS(443)──> Caddy #1 (mcps-vpn-server, IP pública)
                             │ termina TLS real (Let's Encrypt)
                             │ reenvía HTTP plano por el túnel WireGuard
                             ▼
                      mcps-hpc-01 (10.8.0.18)
                             │
                       Caddy #2 (ahí mismo)
                             │ segundo enrutamiento por Host header
              ┌──────────────┼──────────────┬─────────────┐
              ▼               ▼              ▼             ▼
        nginx/Apache/gunicorn/Grafana/etc (los backends reales)
  • Config de Caddy #1: /etc/caddy/Caddyfile + /etc/caddy/tenants/*.caddy.
  • Config de Caddy #2: /mnt/cephfs/hot/caddy/Caddyfile (montado en el contenedor vía container-caddy.service, systemd+podman en .18).
  • mcps-net (red podman en .18) tiene dns_enabled: false — por eso el Caddyfile de ahí usa IPs fijas, no nombres de contenedor. Si un contenedor se recrea sin --ip fijo, su IP cambia y hay que actualizar el Caddyfile a mano. Ponga siempre --ip 10.89.0.X fijo a cualquier contenedor nuevo que Caddy vaya a referenciar ahí.

Dominios VPN-only vs. públicos

Los dominios internos (Cockpit, Zammad, pgAdmin, RStudio, Jupyter, Snipe-IT, etc.) apuntan en Route53 a propósito a 10.8.0.18 (IP privada, solo alcanzable por VPN), nunca a la IP pública. Con eso, tls internal (certificado autofirmado) da warning de navegador — el patrón correcto es DNS-01 vía Route53, que da certificado real de Let's Encrypt sin necesitar reachability pública.

Prerrequisitos (infraestructura de una sola vez, ya existe)

  • Usuario IAM dedicado caddy-route53-dns01, con permisos mínimos: route53:ChangeResourceRecordSets y route53:ListResourceRecordSets sobre la zona mcps-epcm.org (Z05793723RVTGN4VZAAUN), más GetChange/ListHostedZones* (globales). Nunca usar las credenciales del usuario Administrator.
  • Imagen Caddy custom con el plugin de Route53 (la oficial no lo trae):
    FROM caddy:builder AS builder
    RUN xcaddy build --with github.com/caddy-dns/route53
    FROM caddy:latest
    COPY --from=builder /usr/bin/caddy /usr/bin/caddy
    
    Imagen: localhost/caddy-route53:latest.
  • Credenciales en /etc/caddy-route53.env (root:root, 600), inyectadas al contenedor vía --env AWS_ACCESS_KEY_ID --env AWS_SECRET_ACCESS_KEY (forma "bare" de podman: toma el valor del entorno del proceso llamador, por eso el EnvironmentFile= en el systemd unit).

Procedimiento: agregar un dominio nuevo a este patrón

  1. En Route53, apunte el registro del dominio a 10.8.0.18 (VPN-only) o a la IP pública correspondiente (público).
  2. En /mnt/cephfs/hot/caddy/Caddyfile, agregue el bloque del sitio con:
    tls {
        dns route53 {
            access_key_id {env.AWS_ACCESS_KEY_ID}
            secret_access_key {env.AWS_SECRET_ACCESS_KEY}
        }
        propagation_delay 20s
        propagation_timeout -1
    }
    
  3. caddy validate sobre el Caddyfile antes de aplicar.
  4. caddy reload (no reinicia el proceso, no tira el resto de sitios).

Por qué propagation_timeout -1 y propagation_delay 20s, no el default

La red de .18 redirige por transparencia todo el tráfico DNS al resolver 132.248.204.1 (red UNAM), sin importar qué servidor le pidas — confirmado con nslookup ... 8.8.8.8 desde dentro de un contenedor, devolviendo NXDOMAIN mientras el registro sí existía y propagaba bien visto desde mcps-vpn-server. El chequeo de propagación propio de Caddy (usando ese resolver secuestrado/con caché negativo viejo) se queda colgado indefinidamente.

  • propagation_timeout -1: Caddy no verifica localmente, deja que Let's Encrypt valide directo desde su red.
  • propagation_delay 20s: espera fija para dar tiempo real a Route53, evita condición de carrera si el registro aún no propagó del todo.

Nota: esto no es una ruta de red real hacia la UNAM, solo el DNS configurado en el router — la salida real a internet de .18 es Telmex (única default route). No hay forma de salir con IP de la UNAM desde esta infraestructura.

Verificación

  • curl -v https://<dominio>.mcps-epcm.org muestra SSL certificate verify ok con emisor Let's Encrypt, no un certificado autofirmado.
  • El sitio responde sin advertencia de certificado en el navegador (dentro de la VPN, si es VPN-only).

Solución de problemas

  • Duplicate Resource Record al reintentar DNS-01: si se mata/reinicia Caddy a medio intento, puede quedar un registro _acme-challenge.<dominio> huérfano en Route53 que choca con el siguiente intento. Bórrelo a mano:
    aws route53 list-resource-record-sets --hosted-zone-id Z05793723RVTGN4VZAAUN \
      --query "ResourceRecordSets[?Name=='_acme-challenge.<dominio>.mcps-epcm.org.']"
    # luego change-resource-record-sets con Action=DELETE y el mismo valor
    
  • El challenge se queda colgado indefinidamente: confirme que el bloque tls tiene propagation_timeout -1 — sin eso, Caddy intenta verificar la propagación usando el resolver secuestrado de la red de .18 y nunca termina.
  • Un contenedor backend cambia de IP y el sitio deja de responder: confirme que el contenedor se recreó con --ip fijo; si no, actualice el Caddyfile a la IP nueva.