# Los certificados de Let's Encrypt de `www` y el apex ## ✅ RESUELTO — 3-ago-2026, 12:03 UTC ``` www.feadulta.com Let's Encrypt YR2 3-ago-2026 -> 1-nov-2026 feadulta.com Let's Encrypt YR2 3-ago-2026 -> 1-nov-2026 wp-nuevo.feadulta.com Let's Encrypt YR1 3-ago-2026 -> 1-nov-2026 ``` **Lo que funcionó** (el "plan B" de Inma, porque la Configuration Rule que se propuso primero **no se puede montar en este plan de Cloudflare**): 1. **Custom rule del WAF con acción *Skip*** para `/.well-known/acme-challenge/`. 2. **Apagar "Always Use HTTPS" a nivel de zona** durante unos minutos, y volver a encenderlo después. Con eso el reto se completa por el puerto 80, donde lo atiende Traefik. **Ya se puede subir la zona a `Full (strict)`.** ### ⚠️ Dos avisos para la próxima, que costaron tiempo **1. `acme.json` escribe `"main": "dominio"` CON espacio.** Un `grep '"main":"'` da vacío siempre y parece que no hay certificados. Los certificados de hoy llevaban **casi una hora emitidos** mientras un bucle de vigilancia mal escrito informaba de "sin novedad". **Parsear el JSON, no grepear:** ```bash docker exec coolify-proxy cat /traefik/acme.json > /tmp/a.json python3 -c " import json d=json.load(open('/tmp/a.json')) for r,v in d.items(): if isinstance(v,dict): for c in (v.get('Certificates') or []): print(c.get('domain',{}).get('main')) " ``` **2. Se descartó un riesgo que se había anotado aquí: el modo `Full` NO impide el reto.** Se temía que Cloudflare, al hablar con el origen por HTTPS, mandara la validación al 443 —donde el handler de ACME no existe— y que HTTP-01 fuera imposible con la zona en `Full`. **Es falso**, y se midió: con "Always Use HTTPS" apagado, una petición HTTP a la ruta del reto a través de Cloudflare devuelve **404 con 0 bytes y `server: cloudflare`**, es decir, Cloudflare **sí reenvía al puerto 80** y responde el handler de Traefik. --- *Lo que sigue es el diagnóstico, que se conserva porque explica cómo se llegó hasta aquí y contiene un par de trampas que conviene no repetir.* ## Resumen del problema (histórico) Traefik no conseguía el certificado de `www.feadulta.com` ni de `feadulta.com`. **No había nada roto para los visitantes**: Cloudflare presentaba su propio certificado, y hacia el origen —con el modo SSL de la zona en `Full`— aceptaba el certificado por defecto de Traefik. El único coste era que la zona no podía subir a `Full (strict)`. La causa: el reto HTTP-01 acababa llegando por HTTPS y Cloudflare lo bloqueaba con un 403. ## 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. ### Sobre los visitantes durante la ventana Apagar "Always Use HTTPS" unos minutos resultó inocuo: Cloudflare reenvía al puerto 80 del origen y **Traefik devuelve su propio 302 a HTTPS**, así que el visitante acaba en HTTPS igual. Medido antes y durante. ### 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ó ⚠️ **No con `grep`** — ver el aviso del principio: el fichero escribe `"main": "dominio"` con espacio y un patrón sin espacio da vacío siempre. Parsear el JSON. ## 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. ## `Full (strict)` Ya se puede: los tres certificados están emitidos. Sube la seguridad porque valida además el certificado del origen. **Comprobar el sitio justo después de cambiarlo.**