manuales(RUN-20260819-A3B63B): Caddy: arquitectura de dos capas y alta de dominios con DNS-01/Route53
CI / test-and-build (push) Successful in 12s

This commit is contained in:
2026-08-19 14:25:10 -06:00
parent 7242dd78e7
commit 15ee7a7acc
@@ -0,0 +1,99 @@
+++
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):
```dockerfile
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:
```caddy
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:
```bash
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.