Files
feadulta/docs/cutover/certificados-letsencrypt-cloudflare.md
rafa d98f99d0ab certificados: emitidos, y las dos trampas del camino
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>
2026-08-03 09:17:31 -04:00

185 lines
8.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.**