Ataques de fuerza bruta en WordPress: asegurar el inicio de sesión sin dejarte fuera

¿Te fue útil?

Algunos días tu sitio WordPress funciona bien. Luego, una mañana la CPU está al máximo, PHP-FPM parece estar haciendo CrossFit y tus registros son un scroll interminable de POST /wp-login.php. Los usuarios reales no pueden iniciar sesión. La tasa de aciertos de la caché se desploma. El ataque no es “avanzado” tanto como “incesante”.

La parte complicada no es bloquear la fuerza bruta. Es bloquear la fuerza bruta sin convertir tu propio inicio de sesión en un auto-denegación de servicio. Endurecer el inicio de sesión de WordPress es fácil hacerlo mal y sorprendentemente aburrido hacerlo bien. Hagámoslo bien.

Hechos e historia interesantes (el porqué del problema)

  • wp-login.php se convirtió en un objetivo caliente porque es predecible. A los atacantes les encantan las URL estables; WordPress facilitó el acceso administrativo y por tanto su enumeración.
  • XML-RPC (xmlrpc.php) se introdujo para permitir publicación remota. También dio a los atacantes un método para agrupar intentos de autenticación con system.multicall.
  • “Admin” como nombre de usuario solía ser común. Instalaciones antiguas de WordPress lo tenían por defecto, y los hábitos viejos mueren despacio—especialmente en sitios de staging olvidados.
  • La fuerza bruta suele ser una misión secundaria. Muchas campañas no intentan romper tu sitio específicamente; barren Internet en busca de contraseñas débiles para construir botnets.
  • Credential stuffing supera a la fuerza bruta. Los atacantes usan con frecuencia pares usuario/contraseña filtrados; tu “política de contraseñas fuertes” no ayuda si los usuarios reutilizan contraseñas en otros sitios.
  • La limitación de tasa es anterior a la mayoría de frameworks web. Los ingenieros de red ya estaban limitando clientes abusivos mucho antes de que “WAF” fuera una categoría de producto.
  • Los bloqueos pueden convertirse en una denegación de servicio. Un “bloquear después de 3 intentos por 24 horas” ingenuo puede permitir que atacantes bloqueen usuarios reales intentando sus nombres de usuario.
  • 403 vs 404 importa operativamente. Devolver 404 para /wp-login.php a todo el mundo excepto a orígenes allowlist reduce el ruido de escaneo y hace los logs más fáciles de razonar.
  • La mayoría de las compromisos de WordPress no provienen de fuerza bruta en el login. Plugins y temas vulnerables son una fuente mayor de compromisos reales; endurecer el login sigue siendo necesario, pero no suficiente.

Broma #1: Los atacantes de fuerza bruta son como niños frente a una puerta cerrada—infinitamente optimistas, ruidosos y convencidos de que el problema eres .

Cómo se ve la fuerza bruta en producción

Señales que verás primero

En un VPS pequeño, la fuerza bruta aparece como picos en la carga y trabajadores PHP saturados. En un host más grande es más sutil: aumento de 499/504, TTFB elevado y latencia en endpoints de login. El síntoma visto por usuarios suelen ser “no puedo iniciar sesión” o “el sitio está lento”, pero tu problema real es que cada intento de inicio de sesión dispara rutas de código costosas: hashing de contraseñas, creación de sesión, accesos a BD, quizá hooks de plugins que no deberían estar en la ruta.

Dónde atacan

  • /wp-login.php — login clásico por formulario.
  • /wp-admin/ — redirige al login; sigue generando trabajo.
  • /xmlrpc.php — endpoint de publicación remota, a menudo abusado para adivinar contraseñas.
  • Endpoints REST que filtran usuarios — no es una omisión de autenticación, pero ayuda a enumerar usuarios.

Por qué “simplemente bloquear IPs” falla

Los ataques están distribuidos. Las IPs rotan. Algunas vienen de redes residenciales. Otras de proveedores cloud. Si tu estrategia exige bloqueos manuales de IP, no estás defendiendo—estás haciendo arqueología de logs.

Tu objetivo real

Buscas reducir los intentos de autenticación a una tasa baja y predecible mientras garantizas que mantienes al menos dos formas independientes de recuperar el acceso de administrador. Esa segunda parte es donde la mayoría se estampa.

Una cita operativa para pegar en un recordatorio: “La esperanza no es una estrategia.” — Gene Kranz

Guía de diagnóstico rápido (primero/segundo/tercero)

Esta es la versión “estás de guardia, son las 02:00 y alguien de marketing está despierto”.

Primero: confirma que es fuerza bruta e identifica el punto de entrada

  • Revisa los registros de acceso para rutas principales y tasas de petición.
  • Confirma si es wp-login.php, xmlrpc.php, o ambos.
  • Busca un bucle cerrado: misma URI, mismo método, muchos 200/302/403.

Segundo: protege la capacidad antes de perseguir elegancia

  • Añade un límite de tasa temporal en el borde (CDN/WAF) o en Nginx/Apache.
  • Si es necesario, bloquea xmlrpc.php temporalmente y valida que nada crítico falle.
  • Aumenta la claridad del logging: separa endpoints de autenticación en su propio formato o archivo de logs.

Tercero: implementa controles duraderos con una ruta de recuperación

  • Allowlist las IPs administrativas (o mejor: exigir VPN / acceso zero-trust) para /wp-admin/ y /wp-login.php.
  • Habilita 2FA para cuentas administrativas.
  • Instala Fail2ban (o equivalente) accionado por logs web.
  • Desactiva o restringe XML-RPC y confirma workflows de Jetpack/móvil si los usas.

El cuello de botella normalmente no es “la CPU está lenta”. Es “tu ruta de autenticación no tiene throttling”. Arregla eso primero; luego optimiza.

Tareas prácticas: comandos, salidas y decisiones

Estas tareas asumen Linux con Nginx o Apache, además de systemd. Ejecútalas en el host web (o en el proxy inverso si tienes uno). Cada tarea incluye: comando, salida de ejemplo, qué significa y la decisión que tomas.

Tarea 1: Identificar rápidamente las rutas más solicitadas

cr0x@server:~$ sudo awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head
  48219 /wp-login.php
  11703 /xmlrpc.php
   4122 /
   3088 /wp-admin/
    944 /wp-json/wp/v2/users

Qué significa la salida: /wp-login.php domina. /xmlrpc.php también está siendo golpeado. El endpoint REST users sugiere enumeración de usuarios.

Decisión: Priorizar limitación de tasa y control de acceso en wp-login.php y xmlrpc.php. Considerar restringir exposición del REST users.

Tarea 2: Medir la tasa de peticiones a wp-login.php

cr0x@server:~$ sudo awk '$7=="/wp-login.php"{print $4}' /var/log/nginx/access.log | cut -d: -f1-3 | sort | uniq -c | sort -nr | head
   982 [27/Dec/2025:01
   944 [27/Dec/2025:00
   901 [26/Dec/2025:23

Qué significa: Aproximadamente 900–980 accesos al login por hora (o por minuto, dependiendo de la granularidad del formato de log). En cualquier caso: demasiado para un sitio real.

Decisión: Establecer un límite inicial (ejemplo: 5 solicitudes/minuto por IP a wp-login.php) y observar daños colaterales.

Tarea 3: Encontrar las IPs ofensivas principales

cr0x@server:~$ sudo awk '$7=="/wp-login.php"{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head
   710 203.0.113.44
   688 198.51.100.72
   621 192.0.2.19

Qué significa: Unas pocas IPs son ruidosas. Muchos ataques no están tan concentrados, pero cuando lo están, puedes ganar tiempo con bloqueos temporales.

Decisión: Usar un bloqueo de firewall de corta duración solo como medida temporal. No construir un proceso alrededor del bloqueo manual.

Tarea 4: Comprobar si XML-RPC está siendo abusado vía multicall

cr0x@server:~$ sudo grep -c "POST /xmlrpc.php" /var/log/nginx/access.log
11703

Qué significa: El endpoint está activo. Para confirmar multicall, inspecciona cuerpos de petición en el WAF/proxy o habilita logging limitado de cuerpos (con cuidado; puede filtrar credenciales).

Decisión: Si no necesitas XML-RPC explícitamente, desactívalo. Si lo necesitas, restríngelo fuertemente.

Tarea 5: Buscar mezcla de 401/403/200 para entender qué pasa

cr0x@server:~$ sudo awk '$7=="/wp-login.php"{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -nr
  31001 200
  14112 302
   1106 403

Qué significa: Muchas cargas de la página de login devuelven 200. Los 302 indican redirecciones (a menudo redirecciones tras login exitoso, pero también pueden ser redirecciones de vuelta al login en fallos según la configuración).

Decisión: No adivines. Usa logs de la aplicación o logs de autenticación de WordPress (plugin) si necesitas contajes precisos de “contraseña fallida”; de lo contrario limita la tasa de todos modos.

Tarea 6: Confirmar presión en PHP-FPM (un cuellos de botella común)

cr0x@server:~$ sudo systemctl status php8.2-fpm --no-pager
● php8.2-fpm.service - The PHP 8.2 FastCGI Process Manager
     Loaded: loaded (/lib/systemd/system/php8.2-fpm.service; enabled; vendor preset: enabled)
     Active: active (running) since Fri 2025-12-26 23:12:08 UTC; 2h 11min ago
   Main PID: 1240 (php-fpm8.2)
     Status: "Processes active: 43, idle: 2, Requests: 184201, slow: 57, Traffic: 1.2req/sec"

Qué significa: Estás cerca del máximo de procesos activos; existen solicitudes lentas. La fuerza bruta puede agotar trabajadores y dejar sin recursos al tráfico real.

Decisión: Limitar la tasa de endpoints de autenticación de inmediato. Solo entonces considera ajustar FPM.

Tarea 7: Inspeccionar logs de error de Nginx por timeouts upstream

cr0x@server:~$ sudo tail -n 8 /var/log/nginx/error.log
2025/12/27 01:18:21 [error] 2011#2011: *9921 upstream timed out (110: Connection timed out) while reading response header from upstream, client: 198.51.100.72, server: example.com, request: "POST /wp-login.php HTTP/1.1", upstream: "fastcgi://unix:/run/php/php8.2-fpm.sock", host: "example.com"

Qué significa: La ruta de autenticación es lo suficientemente lenta como para alcanzar timeouts—clásico bajo fuerza bruta. Además, los timeouts crean reintentos, lo que empeora la carga.

Decisión: Limitar, luego reducir trabajo en la ruta de autenticación (plugins, llamadas externas), y después considerar ajustar timeouts.

Tarea 8: Añadir un rate limit quirúrgico en Nginx para wp-login.php

Primero, revisa la disposición de includes de tu configuración de Nginx. Luego implementa algo mínimo y reversible.

cr0x@server:~$ sudo nginx -T 2>/dev/null | grep -n "http {" -n | head
32:http {

Qué significa: Sabes dónde existe el contexto http; las zonas de limit_req deben definirse allí.

Decisión: Añadir un limit_req_zone bajo http, luego aplicarlo en la location para wp-login.php.

cr0x@server:~$ sudo bash -lc 'cat >/etc/nginx/conf.d/wp-login-rate-limit.conf <<"EOF"
limit_req_zone $binary_remote_addr zone=wp_login:10m rate=5r/m;
EOF'
cr0x@server:~$ sudo bash -lc 'cat >/etc/nginx/snippets/wp-login-protect.conf <<"EOF"
location = /wp-login.php {
  limit_req zone=wp_login burst=10 nodelay;
  include snippets/fastcgi-php.conf;
  fastcgi_pass unix:/run/php/php8.2-fpm.sock;
}
EOF'
cr0x@server:~$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Qué significa: La configuración se parsea. No has demostrado aún el comportamiento, pero es seguro recargar.

Decisión: Recarga y observa respuestas 429 y quejas de usuarios.

cr0x@server:~$ sudo systemctl reload nginx

Tarea 9: Verificar que la limitación de tasa está ocurriendo

cr0x@server:~$ sudo grep -E "limiting requests" /var/log/nginx/error.log | tail -n 3
2025/12/27 01:22:09 [error] 2011#2011: *10441 limiting requests, excess: 5.900 by zone "wp_login", client: 203.0.113.44, server: example.com, request: "POST /wp-login.php HTTP/1.1", host: "example.com"

Qué significa: Nginx está limitando activamente. Eso protege la capacidad.

Decisión: Mantenerlo, pero no quedarse ahí—la limitación sola no evitará el credential stuffing lento para siempre.

Tarea 10: Añadir una puerta allowlist segura para wp-admin (sin romper el front-end)

Este patrón es tosco y efectivo: permitir el acceso administrativo solo desde una IP de egress de VPN o NAT corporativa. No es amigable para usuarios móviles, pero es sensato operativamente para empresas.

cr0x@server:~$ sudo bash -lc 'cat >/etc/nginx/snippets/wp-admin-allowlist.conf <<"EOF"
location ^~ /wp-admin/ {
  allow 203.0.113.10;
  allow 203.0.113.11;
  deny all;
  try_files $uri $uri/ /index.php?$args;
}
EOF'
cr0x@server:~$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Qué significa: Sintaxis OK. Recuerda que /wp-admin/admin-ajax.php es usado por temas/plugins en el front-end.

Decisión: Eximir explícitamente admin-ajax.php de la allowlist si tu sitio lo usa públicamente.

cr0x@server:~$ sudo bash -lc 'cat >/etc/nginx/snippets/wp-admin-ajax-open.conf <<"EOF"
location = /wp-admin/admin-ajax.php {
  include snippets/fastcgi-php.conf;
  fastcgi_pass unix:/run/php/php8.2-fpm.sock;
}
EOF'

Tarea 11: Deshabilitar XML-RPC a nivel web (rápido) y confirmar impacto

cr0x@server:~$ sudo bash -lc 'cat >/etc/nginx/snippets/disable-xmlrpc.conf <<"EOF"
location = /xmlrpc.php {
  return 403;
}
EOF'
cr0x@server:~$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Qué significa: Esto bloquea XML-RPC completamente. Jetpack, algunas apps móviles y ciertas integraciones pueden depender de él.

Decisión: Si necesitas XML-RPC, no lo desactives—restríngelo a IPs conocidas o exige verificaciones adicionales. De lo contrario, mantenlo apagado.

Tarea 12: Instalar y validar Fail2ban contra patrones de wp-login

cr0x@server:~$ sudo apt-get update -y && sudo apt-get install -y fail2ban
Reading package lists... Done
Building dependency tree... Done
fail2ban is already the newest version (1.0.2-2).

Qué significa: Fail2ban está disponible. Ahora necesitas un filtro y una jail ajustada a tu formato de logs.

Decisión: Usa un tiempo de ban corto y un findtime razonable; ya estás limitando la tasa, esto es para ofensores repetidos y reducción de ruido distribuido.

cr0x@server:~$ sudo bash -lc 'cat >/etc/fail2ban/filter.d/nginx-wp-login.conf <<"EOF"
[Definition]
failregex = ^<HOST> - .* "POST /wp-login\.php HTTP/1\.[01]" (200|302) .*
ignoreregex =
EOF'
cr0x@server:~$ sudo bash -lc 'cat >/etc/fail2ban/jail.d/nginx-wp-login.local <<"EOF"
[nginx-wp-login]
enabled = true
port = http,https
filter = nginx-wp-login
logpath = /var/log/nginx/access.log
maxretry = 10
findtime = 600
bantime = 3600
EOF'
cr0x@server:~$ sudo fail2ban-client reload
OK
cr0x@server:~$ sudo fail2ban-client status nginx-wp-login
Status for the jail: nginx-wp-login
|- Filter
|  |- Currently failed: 2
|  |- Total failed:     118
|  `- File list:        /var/log/nginx/access.log
`- Actions
   |- Currently banned: 3
   |- Total banned:     7
   `- Banned IP list:   198.51.100.72 203.0.113.44 192.0.2.19

Qué significa: Tienes baneos automáticos. Esto no es “seguridad resuelta”, pero reduce significativamente el ruido.

Decisión: Mantén bans moderados; no banees accidentalmente el NAT de tu oficina por una hora porque alguien olvidó su contraseña.

Tarea 13: Confirmar usuarios de WordPress y eliminar riesgos obvios

cr0x@server:~$ cd /var/www/html && sudo -u www-data wp user list --fields=ID,user_login,roles
+----+------------+----------------------+
| ID | user_login | roles                |
+----+------------+----------------------+
|  1 | admin      | administrator        |
| 12 | editor1    | editor               |
| 17 | seo-team   | administrator        |
+----+------------+----------------------+

Qué significa: Si todavía tienes admin como login, facilitas la vida a los atacantes.

Decisión: Crea un nuevo usuario admin con un login no obvio, migra privilegios y elimina o degrada admin. (También: habilita 2FA.)

Tarea 14: Recuperación de emergencia — resetear una contraseña de admin de forma segura

cr0x@server:~$ cd /var/www/html && sudo -u www-data wp user update admin --user_pass='REDACTED-strong-password'
Success: Updated user 1.

Qué significa: Puedes recuperar el acceso sin tocar phpMyAdmin ni editar la base de datos manualmente.

Decisión: Si no tienes WP-CLI instalado, instálalo ahora—antes de necesitarlo a las 02:00.

Tarea 15: Verificar que no te bloqueaste a ti mismo (la prueba aburrida pero necesaria)

cr0x@server:~$ curl -I -s https://example.com/wp-login.php | head -n 5
HTTP/2 200
date: Sat, 27 Dec 2025 01:28:11 GMT
content-type: text/html; charset=UTF-8
cache-control: no-store, no-cache, must-revalidate, max-age=0

Qué significa: El endpoint es accesible desde donde estás probando. Si aplicaste allowlists, prueba desde una red permitida también.

Decisión: Ejecuta una segunda prueba desde una IP no permitida (o usa el LTE de un compañero) para confirmar que obtienes el comportamiento de denegación esperado.

Patrones de endurecimiento que no te bloquean

1) Prefiere controles “puerta” sobre controles “adivinanza”

La limitación de tasa y Fail2ban reaccionan al comportamiento. Las allowlists y requisitos de VPN impiden la exposición por completo. Si este es un admin de WordPress corporativo usado por empleados, la jugada correcta es simple: no expongas wp-admin al internet público. Ponlo detrás de VPN, proxy con identidad o al menos una lista de IPs permitidas.

Si gestionas un sitio comunitario con muchos administradores móviles, no puedes confiar en allowlists. Entonces apuesta más por: 2FA, autenticación sin contraseña (cuando esté disponible) y límites de tasa estrictos.

2) Haz que wp-login sea barato de procesar

Incluso con límites, quieres que cada petición cueste lo menos posible.

  • Mantén la ruta de login libre de plugins pesados que enganchen la autenticación y llamen APIs externas.
  • Asegura que el object caching funcione para reducir la carga en la BD en peticiones repetidas.
  • Desactiva endpoints de enumeración de usuarios (cuando sea práctico) para que los atacantes no puedan confirmar usuarios barato.

3) Decide explícitamente qué hacer con XML-RPC

XML-RPC no es “malo”. Es antiguo y frecuentemente innecesario. Si no usas Jetpack, la app móvil de WordPress o integraciones legacy, bloquealo.

Si lo necesitas, restríngelo:

  • Allowlist solo IPs conocidas (si tu integración tiene egress estable).
  • Limitalo por separado de wp-login.php.
  • Prefiere controles a nivel de aplicación que deshabiliten el abuso de system.multicall (algunos plugins de seguridad lo hacen).

4) Usa 2FA en serio

2FA no detiene los intentos de fuerza bruta; hace que un brute force exitoso sea mucho menos probable. El beneficio operativo es enorme: puedes ser un poco menos agresivo con los bloqueos sin aumentar el riesgo.

Dos reglas operativas:

  • Exigir 2FA para cuentas administradoras al menos.
  • Mantener códigos de respaldo en tu gestor de contraseñas accesibles para la rotación de guardia.

5) No “ocultes” wp-login como defensa primaria

Cambiar la URL de login puede reducir ruido. No resuelve el problema subyacente y puede romper integraciones, caches y runbooks operativos. Úsalo solo como medida secundaria, y solo si tienes un plan de recuperación sólido.

6) Dáte al menos dos rutas de recuperación

Los bloqueos son sobrevivibles si los planeas. Elige dos (o más) de estas opciones y pruébalas trimestralmente:

  • Acceso WP-CLI como www-data (o el usuario FPM correcto) para resetear contraseñas y gestionar plugins.
  • Perfil VPN “break-glass” de emergencia guardado en un vault seguro.
  • Un host bastión con IP estable allowlistada para endpoints administrativos.
  • Procedimiento de acceso a la base de datos para crear un usuario admin (último recurso; peligroso en apuros).

Broma #2: Lo único más fácil que bloquear a los atacantes es bloquear a tu CEO cinco minutos antes de una actualización al consejo.

Tres mini-historias corporativas (cómo la gente falla realmente)

Mini-historia 1: El incidente causado por una suposición equivocada

La Compañía A tenía una instalación WordPress que “no era importante”. Alojó un blog de empleos y algunas landing pages. El producto principal estaba en otro sitio, así que el host de WordPress vivía en un rincón abandonado de la infraestructura.

Un ingeniero notó intentos de fuerza bruta y añadió una regla agresiva: bloquear cualquier IP que golpeara /wp-login.php más de tres veces al día. Parecía seguro; “los usuarios reales no fallan la contraseña tanto”, razonaron. La suposición equivocada no fue técnica, fue humana. Los usuarios reales sí fallan la contraseña tanto, sobre todo después de una migración SSO, un cambio de gestor de contraseñas o unas largas vacaciones.

El atacante no necesitó adivinar contraseñas. Solo necesitó una lista de usuarios. Intentaron tres logins para cada nombre de usuario admin conocido desde un conjunto rotativo de IPs. El plugin de seguridad bloqueó diligentemente cada cuenta admin por 24 horas. De pronto nadie pudo publicar ofertas, y los reclutadores empezaron a enviar capturas de pantalla de “cuenta bloqueada” como si fuera un bug del producto.

Arreglarlo requirió revertir la política de bloqueo, desbloqueos manuales y un nuevo diseño: limitación por IP (no por usuario), ventanas de ban moderadas, 2FA y acceso admin detrás de VPN. La lección: la defensa debe castigar bots, no humanos.

Mini-historia 2: La optimización que salió mal

La Compañía B ejecutaba WordPress detrás de un proxy inverso y quería reducir la carga backend. Alguien activó caching de página completa agresivamente e intentó “cachearlo todo”. El sitio se volvió más rápido—hasta la siguiente ola de fuerza bruta.

La capa de caché no almacenó wp-login.php (bien), pero sí cacheó algunas páginas de redirección y error de forma extraña. Bajo carga, clientes empezaron a recibir redirecciones inconsistentes entre /wp-admin/ y /wp-login.php. Algunos usuarios quedaron atrapados en bucles. Mientras tanto, las claves de cache del proxy no incluían un header que distinguiera ciertas respuestas relacionadas con auth. El proxy sirvió lo incorrecto rápidamente.

Empeoró: el proxy estaba configurado para reintentar automáticamente errores upstream. Cuando PHP-FPM empezó a hacer timeouts bajo fuerza bruta, el proxy reintentó. Eso duplicó el tráfico en el endpoint más caliente. Todos querían hacer lo correcto; el sistema no tuvo piedad.

La solución fue “menos ingeniosa, más correcta”: evitar cache para todas las rutas de auth, desactivar reintentos automáticos para solicitudes no idempotentes como POST al login, e implementar rate limits en el proxy. El rendimiento volvió y el comportamiento fue predecible. Moral: el caching no es un control de seguridad, y los reintentos no son gratuitos.

Mini-historia 3: La práctica aburrida pero correcta que salvó el día

La Compañía C tenía un hábito mundano: cada cambio de infraestructura a WordPress incluía una checklist de recuperación probada. No una wiki que nadie lee—un runbook real con un ensayo trimestral. Dos admins practicaban: “simular bloqueo”, “recuperar acceso vía WP-CLI”, “validar backups de 2FA”, “confirmar que la allowlist funciona”, “confirmar que el tráfico no admin no se ve afectado”.

Cuando una nueva regla WAF se desplegó globalmente, empezó a desafiar logins con un chequeo de bots que no funcionaba bien con ciertas redes corporativas. Los usuarios se quejaron de que el login “se queda cargando para siempre”. El equipo WAF sospechó inicialmente del host de WordPress, porque así suelen empezar estas historias.

El ingeniero on-call siguió el runbook. Primero, validar salud del backend. Segundo, comprobar códigos de estado de los endpoints de auth. Tercero, comparar comportamiento desde un bastión permitido y desde un cliente normal. La diferencia señaló directamente al WAF. Revirtieron la regla específica para wp-login.php mientras mantenían rate limits y protecciones de bot para el resto del sitio.

Sin heroísmos. Sin adivinanzas. Solo una ruta de recuperación practicada y alcance controlado. La parte “aburrida”—simulacros rutinarios—fue exactamente por qué nadie pasó la noche debatiendo reglas de firewall en Slack.

Errores comunes: síntomas → causa raíz → solución

1) Síntoma: Los admins no pueden iniciar sesión aleatoriamente; picos de tickets de soporte

Causa raíz: Bloqueos por nombre de usuario (o bloqueos globales demasiado estrictos) permiten que atacantes bloqueen cuentas legítimas sin conocer contraseñas.

Solución: Limitar por IP y por endpoint, no por nombre de usuario. Mantén bans más cortos. Exige 2FA para admins. Usa una ruta de admin allowlist (VPN/bastión).

2) Síntoma: El sitio está lento; PHP-FPM alcanzó max children

Causa raíz: Intentos de login sin throttling agotan trabajadores PHP; hooks de auth costosos amplifican el coste.

Solución: Añadir limitación a nivel web (limit_req o equivalente), luego auditar plugins por hooks de autenticación y eliminar los pesados de la ruta de login.

3) Síntoma: Bloquear wp-admin rompe funcionalidades del front-end

Causa raíz: /wp-admin/admin-ajax.php es usado por componentes públicos; bloquear todo /wp-admin/ rompe AJAX públicos.

Solución: Permitir acceso público a admin-ajax.php (y a otros endpoints necesarios) mientras restringes el resto de /wp-admin/.

4) Síntoma: Desactivaste XML-RPC y algo “misteriosamente” dejó de funcionar

Causa raíz: Jetpack, apps móviles o integraciones legacy dependían de XML-RPC.

Solución: O bien reactivar con allowlisting/rate limiting o reemplazar la integración por alternativas REST. Haz de XML-RPC una decisión consciente.

5) Síntoma: Añadiste un challenge WAF y los logins hacen bucle o fallan silenciosamente

Causa raíz: Los challenges interativos pueden romper flujos POST, cookies o clientes no-browser; a veces proxies corporativos quitan headers.

Solución: Eximir /wp-login.php de challenges interactivos; usa rate limits y reglas de reputación en su lugar. Prueba desde múltiples redes.

6) Síntoma: Fail2ban banea a todos detrás de un NAT

Causa raíz: Muchos usuarios comparten una IP pública; maxretry se dispara por agregado.

Solución: Aumentar umbrales, acortar bans y confiar más en rate limiting. Considera poner admin detrás de VPN para reducir colisiones de IP compartida.

7) Síntoma: “Ocultaste” wp-login y ahora integraciones fallan y nadie recuerda la URL

Causa raíz: Seguridad por oscuridad sin higiene operativa.

Solución: Si cambias la URL de login, documenta en un runbook interno y mantén un camino break-glass (WP-CLI + VPN/bastión). Trátalo como un reductor de ruido opcional, no como defensa primaria.

Listas de verificación / plan paso a paso

Fase 0 (hoy): detener la hemorragia sin romper cosas

  1. Confirma endpoints objetivo en logs: wp-login.php, xmlrpc.php, wp-admin.
  2. Añade limitación a nivel web a wp-login.php (y xmlrpc.php si está habilitado).
  3. Desactiva XML-RPC si no lo necesitas. Si lo necesitas, restríngelo en lugar de dejarlo abierto.
  4. Revisa saturación de PHP-FPM y asegúrate de que los timeouts no estén provocando reintentos en cascada.
  5. Verifica que el login admin real siga funcionando desde al menos dos redes.

Fase 1 (esta semana): reducir exposición y añadir controles de identidad

  1. Poner wp-admin detrás de una puerta: VPN, proxy con identidad o allowlisting de IPs.
  2. Exigir 2FA para administradores y almacenar códigos de respaldo en un vault compartido y controlado.
  3. Eliminar cuentas obvias: quitar admin, auditar usuarios admin inactivos, aplicar principio de menor privilegio.
  4. Desplegar Fail2ban ajustado a tu formato de logs y realidades de NAT.
  5. Añadir monitorización para picos en peticiones a endpoints de auth, 429s y agotamiento de trabajadores FPM.

Fase 2 (este mes): hacerlo resistente y testeable

  1. Crear un runbook break-glass: reset de contraseñas con WP-CLI, deshabilitar plugins, pasos para rollback de WAF.
  2. Ejecutar un simulacro de bloqueo: simular fallo de allowlist y recuperar.
  3. Revisar superficie de ataque de plugins/temas; actualizar, eliminar abandonware y reducir hooks de autenticación.
  4. Considerar mover endpoints de auth detrás de protecciones separadas (reglas en el edge, límites más estrictos, logging dedicado).

Preguntas frecuentes

1) ¿Debería bloquear wp-login.php por completo?

Si tienes una ruta administrativa controlada (VPN/bastión/proxy de identidad), sí—bloquéalo para Internet público y permite solo la puerta. Si tienes admins móviles, no lo bloquees totalmente; usa rate limit + 2FA + Fail2ban.

2) ¿Vale la pena cambiar la URL de login?

Reduce ruido, no riesgo. Úsalo solo después de tener limitación de tasa, 2FA y un plan de recuperación probado. Si no, sólo cambias un problema por un nuevo incidente “¿dónde está el login?”.

3) ¿Por qué veo fuerza bruta incluso en un sitio pequeño y poco conocido?

Porque es escaneo automatizado. Los atacantes no necesitan saber que existes; solo que WordPress existe. Los endpoints predecibles son sus preferidos.

4) ¿La 2FA detendrá los intentos de fuerza bruta?

No. Evita muchos inicios de sesión exitosos. Aún necesitas limitación de tasa para proteger capacidad y que la auth no se convierta en denegación de servicio.

5) ¿Puedo desactivar XML-RPC con seguridad?

A menudo sí. Pero comprueba si usas Jetpack, la app móvil de WordPress o integraciones antiguas. Si no estás seguro, desactívalo en una ventana de baja carga y vigila logs y flujos de negocio.

6) ¿Por qué bloquear /wp-admin/ a veces rompe la página principal?

Generalmente rompe funciones que llaman a /wp-admin/admin-ajax.php desde el front-end. Exime ese endpoint (o migra lejos del uso de admin-ajax si es posible).

7) ¿Es Fail2ban suficiente por sí solo?

No. Es una buena segunda línea. La primera línea es la limitación a nivel web y reducir exposición. Fail2ban también lucha con ataques altamente distribuidos y entornos NAT compartidos.

8) ¿Cómo evito bloquearme al añadir allowlists?

Siempre mantén un segundo método de acceso: un bastión con IP estable, o VPN. Valida desde redes permitidas y no permitidas antes de retirarte. Y ten WP-CLI listo para resetear credenciales.

9) ¿Cuál es un límite de tasa sensato para wp-login.php?

Empieza con ~5 peticiones/minuto por IP con un pequeño burst, luego afina. Si tienes muchos usuarios legítimos detrás de un NAT, aumenta burst o tasa y apóyate más en 2FA y capa de reputación del WAF.

10) ¿Y si el atacante usa credenciales válidas (credential stuffing)?

Rate limiting reduce la velocidad, 2FA previene la mayoría de tomas de cuenta, y la monitorización (logins fallidos, geos inusuales, travel impossible) detecta lo que pase. Además: obliga al uso de gestores de contraseñas y elimina cuentas inactivas.

Próximos pasos que puedes hacer hoy

Haz tres cosas y el inicio de sesión de WordPress dejará de ser un saco de boxeo:

  1. Limita la tasa de endpoints de autenticación a nivel web. Protege la capacidad de inmediato.
  2. Pon wp-admin detrás de una puerta (VPN/bastión/proxy de identidad) si es un admin empresarial, no un admin comunitario público.
  3. Mantén rutas de recuperación reales: acceso WP-CLI, códigos de respaldo de 2FA y un runbook probado. “Siempre podemos hacer SSH” no es un plan de recuperación; es un cuento para dormir.

Luego toma la victoria poco glamorosa: audita cuentas admin, elimina nombres obvios, desactiva lo que no usas (especialmente XML-RPC) y monitoriza las tasas de peticiones de login como monitorizas el espacio en disco. No porque sea emocionante. Porque funciona.

← Anterior
Regla del 80% de ZFS: mito, verdad y la verdadera ‘zona peligrosa’
Siguiente →
Proxmox “TASK ERROR: timeout waiting for …”: localizar qué se agotó realmente

Deja un comentario