docs: por que no salen los certificados de www y el apex

Documento para que lo pueda leer Inma sin tener que reconstruir la conversacion.

Lo esencial: el lado del servidor esta bien. Traefik intercepta la ruta del reto
en el entrypoint http y no la redirige, y se ve en que ese 404 viene vacio y sin
cabecera server mientras cualquier otra ruta da 302. El problema esta delante, en
el borde de Cloudflare: el reto acaba llegando por HTTPS y lo para un 403.

Se recoge tambien por que dos personas midiendo lo mismo obtuvieron resultados
distintos, que es lo que mas tiempo nos ha costado: Cloudflare devuelve 403 a
nuestras IPs para cualquier hostname de feadulta, y con el User-Agent de Let's
Encrypt deja pasar el bloqueo y entonces aparece el 301. La forma del token no
cambia nada. Conclusion util: el unico testigo que vale es lo que reporta Let's
Encrypt, no nuestros curl.

Y un aviso que puede hacernos perder la tarde: Let's Encrypt permite 5
validaciones fallidas por hostname y hora, www toco el limite a las 11:47 UTC y
cada redeploy quema tres. Reintentar antes de que pase la hora falla por rate
limit aunque la regla este bien puesta, y nos haria creer que no funciona. Va el
procedimiento en orden: primero la regla, luego esperar, luego un solo reintento.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-03 08:00:45 -04:00
parent 4f91d68996
commit f6170b70b1
@@ -0,0 +1,133 @@
# Los certificados de Let's Encrypt de `www` y el apex no se emiten
**Estado: abierto.** Bloqueado a la espera de una regla en Cloudflare, que gestiona Inma.
Fecha: 3-ago-2026, después del cutover. Plan maestro: `rafa/feadulta`#180.
## Resumen para quien tenga prisa
Traefik no consigue el certificado de `www.feadulta.com` ni de `feadulta.com`. **No hay nada roto
para los visitantes**: Cloudflare presenta su propio certificado, y hacia el origen —con el modo SSL
de la zona en `Full`— acepta el certificado por defecto de Traefik. El único coste es que **la zona
no puede subir a `Full (strict)`** hasta que se resuelva.
La causa es que el reto HTTP-01 de Let's Encrypt acaba llegando por HTTPS y Cloudflare lo bloquea
con un 403. Se arregla con una **Configuration Rule** en Cloudflare acotada a la ruta del reto.
## Lo que dice Let's Encrypt, que es el único testigo que vale
```
2026-08-03T11:46:54Z domains [www.feadulta.com]
invalid authorization: acme: error: 403 :: urn:ietf:params:acme:error:unauthorized ::
Invalid response from https://www.feadulta.com/.well-known/acme-challenge/YnmnUk3wAqkMGkL8Evv49DQrW1euPfnOgNFoekwLSUc: 403
```
Ese intento es **posterior** al cambio de DNS, al arreglo del mu-plugin y a dos redeploys, y lleva un
token real de 43 caracteres. Nótese el **`https`**: el reto empieza por el puerto 80 y termina en el
443.
## Por qué las mediciones desde nuestras máquinas no sirven
⚠️ **Cloudflare devuelve 403 a nuestras IPs para cualquier hostname de feadulta** — la de Rafa y la
del propio Hetzner. Eso contamina cualquier `curl`. Medido:
| petición desde la WSL | resultado |
|---|---|
| token real (43 ch), **sin** User-Agent de LE | 403 |
| token real (43 ch), **con** User-Agent de LE | **301 a https** |
| token de ejemplo corto | 403 |
| directorio `/.well-known/acme-challenge/` a secas | 403 |
Idéntico en apex, `www` y `wp-nuevo`. **La forma del token no cambia nada; lo que cambia es el
User-Agent.** Por eso dos personas midiendo lo mismo obtienen resultados distintos y ninguna de las
dos tiene razón: hay que mirar lo que reporta Let's Encrypt.
*(Se barajó que Cloudflare exime de "Always Use HTTPS" las URLs de challenge que llevan token. En
esta zona **no se está aplicando**: el intento de las 11:46, con token real, terminó en 403.)*
## La cadena, medida contra el origen
| punto | resultado | quién responde |
|---|---|---|
| Origen puerto 80, ruta del reto | 404, **0 bytes**, sin cabecera `server` | el handler de ACME de **Traefik** |
| Origen puerto 80, cualquier otra ruta | 302 a HTTPS | Traefik |
| Origen puerto 443, ruta del reto | 404, 99.802 bytes | WordPress |
O sea que **el lado del servidor está bien**: Traefik intercepta la ruta del reto en el entrypoint
`http` y no la redirige. La configuración lo confirma:
```
--certificatesresolvers.letsencrypt.acme.httpchallenge=true
--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=http
```
El problema está **delante**, en el borde de Cloudflare, no en Hetzner.
## El arreglo
**Cloudflare → Rules → Configuration Rules.** Una regla acotada a la ruta del reto:
- **Ámbito:** `*feadulta.com/.well-known/acme-challenge/*`
- **Always Use HTTPS → Off** — para que el reto se complete por el puerto 80, donde Traefik lo atiende
- **Security Level → Essentially Off** (o una regla WAF de tipo *Skip* para esa ruta)
Es una excepción mínima y acotada: esa ruta no sirve contenido, solo tokens de un solo uso que el
propio Traefik genera y valida.
### Alternativa de fondo, para más adelante
Pasar Traefik a validación **DNS-01** con el token de Cloudflare que ya tenemos
(`FEA_CF_API_TOKEN`, permiso `Zone → DNS → Edit`). Se salta el problema para siempre porque no
depende del HTTP, y es lo recomendado cuando el origen vive detrás de un proxy. **Pero toca la
configuración del proxy de Coolify entero, no solo feadulta**, así que no se hace en caliente ni sin
ensayarlo.
## ⚠️ Límite de Let's Encrypt: no reintentar a lo loco
Let's Encrypt permite **5 validaciones fallidas por hostname y hora**. Fallos del 3-ago:
| hostname | fallos | horas (UTC) |
|---|---|---|
| `www.feadulta.com` | **5** | 10:44, 11:37, 11:38, 11:46, 11:47 |
| `feadulta.com` | 3 | 10:44, 11:38, 11:46 |
| `wp-nuevo.feadulta.com` | 4 | 10:44 ×2, 11:37, 11:46 |
`www` tocó el límite a las 11:47 UTC. **Reintentar antes de que pase la hora falla por rate limit
aunque la regla esté bien puesta**, y hace creer que el arreglo no funciona.
### Procedimiento correcto
1. Inma pone la Configuration Rule.
2. **Esperar a que pase una hora** desde el último fallo (para `www`, a partir de las **12:47 UTC**).
3. Lanzar **un solo** reintento y mirar el resultado antes de tocar nada más.
El reintento obliga a Traefik a recargar configuración, y eso hoy se consigue redesplegando el
servicio:
```bash
curl -s -H "Authorization: Bearer $COOLIFY_API_TOKEN" \
"http://localhost:8000/api/v1/deploy?uuid=r2ssjifwj0r0ghyoqd528uaa&force=false"
```
*(El API de Coolify solo escucha en `localhost:8000` del servidor: la llamada va por SSH.)*
Cuesta **unos 7 segundos** de corte: se ha medido dos veces, un único 500 y vuelta a 200.
**Cada redeploy quema tres validaciones**, una por hostname.
### Comprobar si ya salió
```bash
docker exec coolify-proxy cat /traefik/acme.json | grep -oE '"main":"[^"]*feadulta[^"]*"'
```
Hoy sólo aparecen `antiguo.feadulta.com`, `nuevo.feadulta.com` y `gitea.feadulta.com`.
## Cabo suelto ya cerrado por el camino
`fea-legacy-redirect.php` mandaba **`/.well-known/` al archivo** con un 301, porque manda allí
cualquier 404. No era la causa —el reto va por el 80 y ahí ni llega a WordPress— pero estaba mal y
era una trampa para el futuro. Arreglado y desplegado: las rutas de protocolo quedan exentas y el
resto del redirect sigue igual. Lo encontró el Claude de Inma.
## Cuando el certificado salga
Entonces —y sólo entonces— se puede subir la zona a **`Full (strict)`**, que valida además el
certificado del origen. Hacerlo antes rompe el sitio.