100 lines
6.0 KiB
Markdown
100 lines
6.0 KiB
Markdown
+++
|
|
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.
|