# 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.