20528f0c5e
Aviso de Inma: en su plan las Configuration Rules no ofrecen "Always Use HTTPS" ni "Security Level", asi que la regla del documento no se puede montar tal cual. Se sustituye por lo que si se puede hacer: custom rule del WAF con accion Skip para la ruta del reto, y apagar "Always Use HTTPS" a nivel de zona durante el reintento. Se anade ademas un riesgo que hay que tener presente ANTES de gastar el reintento: con el modo SSL de la zona en Full, Cloudflare habla con el origen por HTTPS. Si eso se aplica tambien a lo que le entra por HTTP, la peticion de Let's Encrypt llegara al 443 de Traefik, donde el handler de ACME no existe, y volvera a fallar. No se puede saber desde aqui sin apagar el ajuste, porque mientras este encendido Cloudflare redirige en su borde y nunca contacta con el origen por el 80. Va una tabla para leer el siguiente error sin discutirlo otra vez: si sale el certificado, bien; si da 403, el Skip no coge esa ruta; si da 404, Cloudflare va al 443 y toca pasar a DNS-01 en vez de seguir quemando validaciones. Y se matiza lo de los visitantes: el 302 del origen solo los cubre si Cloudflare reenvia por el 80. Si va al 443, quien entre por http se queda en http durante la ventana. Inocuo para unos minutos, pero mejor decirlo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
168 lines
8.0 KiB
Markdown
168 lines
8.0 KiB
Markdown
# 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
|
||
|
||
⚠️ **Corrección (Inma, 3-ago): en este plan de Cloudflare las Configuration Rules NO ofrecen
|
||
"Always Use HTTPS" ni "Security Level".** Los ajustes disponibles son Automatic HTTPS Rewrites,
|
||
Browser Integrity Check, Disable RUM, Disable Zaraz, Email Obfuscation, Fonts, Hotlink Protection,
|
||
I'm Under Attack, Opportunistic Encryption, Polish, Request/Response Body Buffering, Rocket Loader y
|
||
SSL. **La regla no se puede montar así.**
|
||
|
||
### Lo que sí se puede hacer
|
||
|
||
1. **Custom rule del WAF con acción *Skip*** para `/.well-known/acme-challenge/` — quita el 403.
|
||
2. **Apagar "Always Use HTTPS" a nivel de zona** durante el reintento, y volver a encenderlo
|
||
después.
|
||
|
||
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.
|
||
|
||
### ⚠️ Puede que aun así no salga, y conviene saber por qué antes de intentarlo
|
||
|
||
Con el modo SSL de la zona en **`Full`, Cloudflare habla con el origen por HTTPS**. Si eso se aplica
|
||
también a las peticiones que le entran por HTTP, entonces al apagar "Always Use HTTPS" la petición
|
||
de Let's Encrypt llegará igualmente al **443** de Traefik, donde **el handler de ACME no existe** —y
|
||
volverá a fallar, ahora con un 404 en vez de un 403.
|
||
|
||
No se puede determinar desde aquí sin apagar el ajuste: mientras "Always Use HTTPS" esté encendido,
|
||
Cloudflare redirige en su borde y **nunca llega a contactar con el origen por el puerto 80**, así que
|
||
no hay forma de observar qué puerto usaría.
|
||
|
||
**Cómo distinguir los dos casos en el siguiente intento**, mirando el error de Traefik:
|
||
|
||
| lo que reporte Let's Encrypt | qué significa | qué hacer |
|
||
|---|---|---|
|
||
| sale el certificado | Cloudflare reenvía por el 80 y Traefik atiende | nada más |
|
||
| `403` | el *Skip* del WAF no está cogiendo esa ruta | revisar el ámbito de la regla |
|
||
| **`404`** | **Cloudflare va al 443 del origen**: HTTP-01 no puede funcionar con la zona en `Full` | **pasar a DNS-01, dejar de quemar validaciones** |
|
||
|
||
### Sobre los visitantes durante la ventana
|
||
|
||
Apagar "Always Use HTTPS" unos minutos es inocuo, pero **no exactamente por el motivo que parece**.
|
||
Si Cloudflare reenviara por el 80, Traefik devuelve un 302 a HTTPS por su cuenta (medido) y el
|
||
visitante acaba igual. Si Cloudflare va al 443, ese 302 no llega a dispararse y **un visitante que
|
||
entre por `http://` se quedará en `http://`** — con el contenido correcto y cifrado hasta Cloudflare,
|
||
pero sin subir a HTTPS. Para una ventana de minutos no tiene consecuencias, pero que nadie se
|
||
extrañe si lo ve.
|
||
|
||
### 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.
|