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>
11 KiB
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.
Modo SSL de la zona— CONFIRMADO POR INMA (2-ago): está enFull, noFlexible. 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.comdeclarado 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):
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.
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 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):
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.
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
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 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
# 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.phpentra 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_backupsde 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.170y a/web/(cierra #158). - Retirar
nuevo.feadulta.comde Cloudflare cuando ya no haga falta.