603eb1ecb4
Aviso de Mixbot (2-ago): los bots de la carta publican por wp-nuevo.feadulta.com, no por www, porque el WAF de Cloudflare los bloquea por www (regla "bloquear bots 1"). Ese hostname no tiene registro propio, lo cubre el wildcard, y Traefik solo enruta nuevo.feadulta.com: al mover el A del apex los bots dejarian de publicar. La carta 738 se compone el martes. Se anade el paso 0 al runbook. Hay que hacerlo en el PANEL: por API el PATCH de fqdn da 422 y actualizar SERVICE_FQDN_WORDPRESS cambia la variable pero no regenera los routers (comprobado hoy). Plan B documentado: los bots pueden pasar a nuevo.feadulta.com, que es grey cloud y por tanto no pasa por Cloudflare ni le aplica ninguna regla del WAF. Decision de Rafa: el bloqueo de login lo lleva Wordfence y sale limit-login-attempts-reloaded. Wordfence hace lo mismo y ademas firewall de aplicacion, escaner de malware y 2FA; los dos juntos son dos contadores compitiendo. Retirado del staging (7 plugins activos, sitio en pie) y anadido al post-import-seguridad.sh (paso 3c) porque el dump lo revive en cada import, junto con sus 12 opciones huerfanas. Nuevo paso 3d que verifica que Wordfence sigue activo y lo reinstala si faltara. Inma confirma que el modo SSL de la zona es Full, no Flexible: cae el riesgo de bucle de redireccion. Marcado como resuelto en los verdes previos. Anadida la seccion del correo: el cutover no lo toca (MX a otra IP), pero la caducidad del 07/08 si, y ediciones@/contenido@ son de donde sale la carta. Anotado tambien que Cloudflare devuelve 403 a la IP de Rafa para cualquier hostname de feadulta: un 403 desde su maquina no prueba nada, la verificacion de los bots la tienen que hacer ellos. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
240 lines
11 KiB
Markdown
240 lines
11 KiB
Markdown
# Runbook del cutover — WordPress feadulta.com: CDMON → Hetzner
|
|
|
|
**Fecha prevista: lunes 3 de agosto de 2026.** Plan maestro: gitea `rafa/feadulta`#180.
|
|
Todos los tiempos de abajo están **medidos en el ensayo del 31-jul**, no estimados.
|
|
|
|
---
|
|
|
|
## Antes de empezar (verde obligatorio)
|
|
|
|
- [ ] **Freeze editorial** avisado a Inma. El cutover no puede caer el día de publicación de la carta.
|
|
*(La carta 738 se compone el martes: el lunes está libre.)*
|
|
- [ ] **CDMON sigue vivo** (§1). Es el rollback: si no está, no se ejecuta.
|
|
- [x] ~~**Modo SSL de la zona**~~ — **CONFIRMADO POR INMA (2-ago): está en `Full`, no `Flexible`.**
|
|
El riesgo de bucle de redirección queda descartado. (*Full (strict)* además valida el
|
|
certificado del origen; es mejor y se puede subir después, no bloquea el cutover.)
|
|
- [ ] 🔴 **`wp-nuevo.feadulta.com` declarado en Coolify — ver paso 0. SIN ESTO SE ROMPEN LOS BOTS.**
|
|
- [ ] **Inventario del wildcard.** La zona tiene `*.feadulta.com` → apex, proxied. Al mover el A del
|
|
apex, **todo subdominio no declarado se va a Hetzner** y Traefik devolverá su 404.
|
|
`info.feadulta.com`: Rafa lo da por residual, se acepta que dé 404.
|
|
|
|
---
|
|
|
|
## 0. 🔴 `wp-nuevo.feadulta.com` — hacer ANTES del cutover
|
|
|
|
**Los bots de la carta (`teologasbot`, `escritoresbot`, `recordatoriobot`) publican por
|
|
`wp-nuevo.feadulta.com`, no por `www`**, porque el WAF de Cloudflare los bloquea por `www` (regla
|
|
"bloquear bots 1"). Aviso de Mixbot, 2-ago.
|
|
|
|
Ese hostname **no tiene registro propio**: lo cubre el wildcard. Al mover el A del apex viaja a
|
|
Hetzner, y **Traefik solo tiene router para `nuevo.feadulta.com`** → devolvería su 404 y los bots
|
|
dejarían de publicar. La carta 738 se compone el **martes**.
|
|
|
|
⚠️ **Hay que hacerlo en el PANEL de Coolify.** Por API no vale: `PATCH /services/{uuid}` con `fqdn`
|
|
da `422 "This field is not allowed"`, y actualizar `SERVICE_FQDN_WORDPRESS` por API cambia la
|
|
variable pero **no regenera los routers de Traefik** (comprobado el 2-ago).
|
|
|
|
Servicio `feadulta-wp` → campo de dominios:
|
|
|
|
```
|
|
antes del cutover: https://nuevo.feadulta.com,https://wp-nuevo.feadulta.com
|
|
después del cutover: https://www.feadulta.com,https://feadulta.com,https://wp-nuevo.feadulta.com
|
|
```
|
|
|
|
**Plan B si el martes wp-nuevo falla:** los bots cambian una línea de su `.env` y publican por
|
|
**`nuevo.feadulta.com`** — es grey cloud, así que **no pasa por Cloudflare y ninguna regla del WAF
|
|
le aplica**, ya tiene certificado y ya está enrutado. Mixbot puede hacerlo al momento.
|
|
|
|
Verificación tras el cutover (no vale hacerla desde la máquina de Rafa: **Cloudflare devuelve 403 a
|
|
esa IP para cualquier hostname de feadulta**, así que un 403 desde ahí no significa nada):
|
|
|
|
```bash
|
|
curl -sI https://wp-nuevo.feadulta.com/wp-json/ | head -3 # desde el Hetzner o desde los bots
|
|
```
|
|
|
|
## Tiempos medidos (ensayo del 31-jul)
|
|
|
|
| Fase | Medido | ¿Se repite el lunes? |
|
|
|---|---|---|
|
|
| Dump CDMON → WSL (105 MB) | 25 s | **Sí** |
|
|
| WSL → Hetzner | 74 s | **Sí** |
|
|
| Saneo de collations + exclusión `*_bak` | 2 s | **Sí** |
|
|
| Import en MySQL 8.4 | 107 s | **Sí** |
|
|
| Instalar plugins + tema + 28 mu-plugins | 4 min 19 s | **No** (ya hecho) |
|
|
| Pre-sync de `uploads` 5,9 GB | ver #180 | **No** (ya hecho; el lunes solo el delta) |
|
|
| **Camino crítico de la ventana** | **≈ 3 min 30 s + delta de uploads** | |
|
|
|
|
> El corte real percibido por el visitante es **solo el cambio del A record**, que en Cloudflare es
|
|
> instantáneo. Todo lo de arriba se hace **antes**, con el sitio viejo aún sirviendo.
|
|
|
|
---
|
|
|
|
## 1. Backup de última hora en el origen
|
|
|
|
No fiarse del cron de UpdraftPlus de las 05:09: forzar uno nuevo o confirmar que el del día existe.
|
|
|
|
```bash
|
|
sshpass -p "$FEA_PROD_SSH_PASS" ssh feadulta@134.0.10.170 \
|
|
'ls -lt /web/wp-content/updraft/*-db.gz | head -3'
|
|
```
|
|
|
|
## 2. Traer el dump del día
|
|
|
|
```bash
|
|
bash scripts/cutover/01-traer-dump.sh # CDMON → WSL → Hetzner, con sha256 en los 3 puntos
|
|
```
|
|
Dos saltos a propósito: `scp`/`sftp` no funcionan contra la jaula de CDMON, y no queremos dejar la
|
|
contraseña de CDMON escrita en el Hetzner.
|
|
|
|
## 3. Sanear el dump ⚠️ el paso que no se puede saltar
|
|
|
|
El origen es **MariaDB 11.8.6** y el destino **MySQL 8.4.10**. El dump trae **20 tablas con
|
|
collations `uca1400_*` que solo existen en MariaDB 11+**; MySQL las rechaza en el `CREATE TABLE` y
|
|
el import aborta.
|
|
|
|
```
|
|
utf8mb4_uca1400_ai_ci → utf8mb4_0900_ai_ci
|
|
utf8mb3_uca1400_ai_ci → utf8mb4_0900_ai_ci (+ CHARSET=utf8mb3 → utf8mb4)
|
|
utf8mb4_unicode_520_ci → sin tocar (válida en MySQL 8.4)
|
|
```
|
|
También excluye las 5 tablas de respaldo (`wp_options_bak`, `wp_posts_bak`, `wp_postmeta_bak`,
|
|
`wp_usermeta_bak`, `wp_fea_date_slug_posts_backup`).
|
|
|
|
**Verificación antes de importar** (las tres deben dar 0):
|
|
```bash
|
|
grep -c 'uca1400' feadulta-saneado.sql
|
|
grep -c 'utf8mb3' feadulta-saneado.sql
|
|
grep -oE '^CREATE TABLE `[^`]+`' feadulta-saneado.sql | grep -cE '_bak|backup'
|
|
```
|
|
|
|
## 4. Importar
|
|
|
|
**`--default-character-set=utf8mb4` en el CLIENTE es obligatorio.** Omitirlo es exactamente lo que
|
|
produjo el mojibake `’` en summaraise.
|
|
|
|
```bash
|
|
docker exec -i mysql-r2ssjifwj0r0ghyoqd528uaa \
|
|
mysql --default-character-set=utf8mb4 -uroot -p"$RP" wordpress < feadulta-saneado.sql
|
|
```
|
|
|
|
Verificación: 29 tablas, y las filas deben cuadrar con
|
|
`(líneas que empiezan por " (") + (nº de sentencias INSERT)` de cada tabla — la primera fila de cada
|
|
`INSERT` va pegada al `VALUES`. **Ignora el `# Approximate rows expected` del dump**: es la
|
|
estimación de InnoDB y para `wp_posts` dice 64.160 cuando las filas reales son 43.941.
|
|
|
|
## 5. Delta de uploads
|
|
|
|
```bash
|
|
rsync -a --info=progress2 --stats --exclude='*.php' \
|
|
-e "sshpass -p ... ssh" feadulta@134.0.10.170:/web/wp-content/uploads/ <destino>/
|
|
```
|
|
Solo mueve lo publicado desde el pre-sync. **`--delete` únicamente tras revisar un `--dry-run`.**
|
|
|
|
## 6. Higiene de seguridad — SIEMPRE después de cada import
|
|
|
|
```bash
|
|
bash scripts/cutover/post-import-seguridad.sh
|
|
```
|
|
El dump trae las cuentas de administrador con sus hashes, **incluidas las tres deshabilitadas en el
|
|
incidente #183**: reimportar las revive. El script rota la contraseña de los 6 administradores
|
|
(leyéndolas de `~/.hermes/profiles/feadulta/.env`), retira el rol a `pabloarias`/`josek`/`andrey`, y
|
|
limpia los cron huérfanos de Yoast y WP Profile Builder.
|
|
|
|
Doble cinturón: además del rol retirado, el mu-plugin `fea-security-blocked-users.php` bloquea
|
|
`wp_authenticate_user` y `allow_password_reset` para esos tres IDs. **Va en el manifiesto de
|
|
`docs/cutover/mu-plugins-manifest.txt`, no en un glob `fea-*`** — un glob se lo dejaría fuera.
|
|
|
|
El script retira además la opción **`wordfence_core_options`**, que **no es de Wordfence**: es el
|
|
payload de SEO spam que dejó el atacante (div oculto con enlaces a un casino, cifrado con un césar
|
|
de una letra). Está inerte —quien lo leía era `wp-highlits`, en cuarentena— pero **viaja dentro del
|
|
dump y cada import lo reintroduce**.
|
|
|
|
## 6b. Plugins de seguridad — Wordfence entra, LLAR sale
|
|
|
|
**Siguen siendo 7 plugins activos**, pero no los mismos: entra **Wordfence** (8.2.2, limpio desde
|
|
wordpress.org) y sale **`limit-login-attempts-reloaded`** (decisión de Rafa, 2-ago).
|
|
|
|
Motivo de que entre Wordfence: CDMON tenía **ModSecurity** delante a nivel de hosting —fue lo que
|
|
detectó las subidas de `wp-cl.php` y `health-check.php` en el #183— y **en Hetzner con Traefik no hay
|
|
WAF ninguno**. Sin él, el servidor nuevo estrenaría con una capa defensiva menos que la que tenía el
|
|
sitio comprometido.
|
|
|
|
Motivo de que salga LLAR: Wordfence hace lo mismo (límite de intentos, bloqueos) **y además**
|
|
firewall de aplicación, escáner de malware y 2FA. Los dos a la vez son dos contadores compitiendo y
|
|
bloqueos difíciles de diagnosticar.
|
|
|
|
**El dump trae LLAR en `active_plugins`, así que cada import lo revive** → lo retira
|
|
`post-import-seguridad.sh` (paso 3c), junto con sus 12 opciones huérfanas. El mismo script comprueba
|
|
que Wordfence sigue activo y lo reinstala si faltara.
|
|
|
|
⚠️ Wordfence queda con **protección básica** (a nivel de plugin). El *extended protection* exige
|
|
configurar `auto_prepend_file` desde su asistente; se hace **después** del cutover y con calma, no
|
|
en la ventana.
|
|
|
|
⚠️ `fea-cloudflare-realip.php` es obligatorio con Wordfence por el mismo motivo que lo era con LLAR:
|
|
sin él ve todas las visitas como si vinieran de las IPs de Cloudflare. Está en el manifiesto.
|
|
|
|
## 7. Quitar el WP_HOME/WP_SITEURL del staging
|
|
|
|
```
|
|
Coolify → servicio feadulta-wp → borrar la variable WORDPRESS_CONFIG_EXTRA → redeploy
|
|
```
|
|
**No hace falta ningún `search-replace`**: la base de datos ya trae `siteurl`/`home` =
|
|
`https://www.feadulta.com`, que es la URL final. La constante era solo para el staging. Esto ahorra
|
|
la pasada más lenta de todo el proceso.
|
|
|
|
## 8. Cambiar el A record en Cloudflare
|
|
|
|
```bash
|
|
# feadulta.com y www.feadulta.com → 188.40.120.157, MANTENIENDO proxied=true (naranja)
|
|
```
|
|
El token **sí tiene permiso de escritura**, verificado el 31-jul creando `nuevo.feadulta.com`.
|
|
Después: purgar la caché de Cloudflare.
|
|
|
|
## 9. Verificación en caliente
|
|
|
|
- [ ] Portada en los 5 idiomas (`/`, `/en/`, `/fr/`, `/it/`, `/pt/`) — `/es/` responde 301, es lo normal.
|
|
- [ ] Carta de la semana, evangelios, EFFA, autores con avatar.
|
|
- [ ] Buscador (índice FULLTEXT) devuelve resultados.
|
|
- [ ] **Audio TTS** suena.
|
|
- [ ] Imágenes cargan (depende del delta de uploads).
|
|
- [ ] `wp-login.php` entra con la contraseña rotada, y **las 3 cuentas del incidente NO entran**.
|
|
- [ ] Certificado Let's Encrypt para apex y `www`.
|
|
- [ ] Cero errores PHP en la portada y en `docker logs`.
|
|
- [ ] GA4 en tiempo real.
|
|
|
|
## 10. Application passwords — con Inma delante, no antes
|
|
|
|
**Solo existe una en todo el sitio**: `publicabot`, del usuario `icalvotorre` (ID 1048), creada el
|
|
27-jun-2026, último uso el 29-jul-2026 desde `213.94.23.1`. Es la que usan los bots de la carta.
|
|
|
|
Rotarla sin coordinar **para la publicación de la carta semanal** y nadie sabrá por qué. Se rota con
|
|
Inma delante, generando la nueva y actualizándola en el bot en el mismo momento.
|
|
|
|
*(Rotar la contraseña de un usuario **no** revoca sus application passwords: el paso 6 no la afecta.)*
|
|
|
|
---
|
|
|
|
## Rollback
|
|
|
|
**Revertir el A record en Cloudflare + purgar caché.** Segundos. El Joomla/WordPress de CDMON sigue
|
|
intacto y sirviendo — comprobado con `curl --resolve` contra `134.0.10.170`.
|
|
|
|
**Esto solo funciona mientras CDMON siga vivo.** Es la razón por la que el §1 no es opcional.
|
|
|
|
## 🔴 El correo — no es del cutover, pero es lo que más puede doler
|
|
|
|
El cutover **no toca el correo**: el MX apunta a `mail.feadulta.com` → `134.0.13.123`, una IP
|
|
distinta de la del web. Mover el A del apex no lo afecta.
|
|
|
|
**Pero la caducidad del 07/08 sí.** Los 17 buzones viven en CDMON, y `ediciones@` y `contenido@` son
|
|
de donde los bots sacan el material de la carta: si cae el correo, se para la carta. Hay que
|
|
resolverlo con CDMON **esta semana**, vaya como vaya el cutover.
|
|
|
|
## Después (no el mismo día)
|
|
|
|
- **Backups en Hetzner: hoy NO HAY NINGUNO.** La tabla `scheduled_database_backups` de Coolify está
|
|
vacía y no hay cron. Summaraise lleva semanas así. Hay que darle dueño.
|
|
- Actualizar los scripts y guías que apuntan a `134.0.10.170` y a `/web/` (cierra #158).
|
|
- Retirar `nuevo.feadulta.com` de Cloudflare cuando ya no haga falta.
|