d98f99d0ab
www.feadulta.com, feadulta.com y wp-nuevo.feadulta.com, de Let's Encrypt y validos hasta el 1-nov. Ya se puede subir la zona a Full (strict). Lo que funciono es el plan B de Inma, porque la Configuration Rule que propuse primero no se puede montar en su plan de Cloudflare: custom rule del WAF con accion Skip para la ruta del reto, mas apagar Always Use HTTPS a nivel de zona unos minutos. Dos cosas que costaron tiempo y quedan anotadas: acme.json escribe "main": "dominio" CON espacio. Un grep sin espacio da vacio siempre y parece que no hay certificados. Los de hoy llevaban casi una hora emitidos mientras mi bucle de vigilancia informaba de "sin novedad", y por eso lanzamos un reintento que no hacia falta. Va el snippet que parsea el JSON. Y se retira un riesgo que yo mismo habia documentado: el modo Full NO impide el reto. Se temia que Cloudflare mandara la validacion al 443, donde el handler de ACME no existe. Es falso y se midio: con Always Use HTTPS apagado, una peticion HTTP a la ruta del reto a traves de Cloudflare devuelve 404 con 0 bytes y server: cloudflare, o sea que reenvia al puerto 80 y responde Traefik. Mejor quitarlo que dejar una advertencia falsa en un documento que va a leer otra gente. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
185 lines
8.2 KiB
Markdown
185 lines
8.2 KiB
Markdown
# 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.**
|