Tu sitio WordPress está “bien” hasta que son las 3:07 AM y el teléfono de guardia vibra como si quisiera atravesar la mesita de noche. El síntoma siempre es el mismo: fallos de inicio de sesión, CPU al máximo, conexiones a la base de datos en cola y una línea en la cronología del incidente que dice “alguien cambió algo relacionado con seguridad”.
Endurecer no es el enemigo. El endurecimiento mal hecho sí lo es. Esta es una lista de verificación de producción para reforzar la seguridad de WordPress sin convertir a editores legítimos, clientes y usuarios de SSO en daños colaterales.
Modelo de amenazas en una página: qué estás defendiendo
El endurecimiento de WordPress se vuelve sensato cuando dejas de tratarlo como una vibra y empiezas a tratarlo como un modelo de amenazas. Los “atacantes” que verás con más frecuencia no son hackers de película. Son bots, malware comercial y oportunistas que rastrean Internet buscando puntos débiles conocidos. Tu trabajo es hacer que tu sitio sea aburrido para atacar y resistente cuando es atacado.
Lo que se ataca más
- /wp-login.php brute force (credential stuffing, password spraying, abuso de tokens de API).
- /xmlrpc.php (históricamente usado para pingbacks y publicación remota; a menudo abusado para amplificación y fuerza bruta).
- Vulnerabilidades de plugins/temas (RCE, subida arbitraria de archivos, bypass de autenticación, CSRF).
- Webroot escribible (un solo fallo de subida de archivos se convierte en persistencia y defacement).
- Deriva de la cadena de suministro (plugins actualizados “cuando sea”, sin rollback, sin canary, sin cambios escalonados).
Qué significa realmente “no rompe los inicios de sesión”
La seguridad de los inicios de sesión no es solo “puedo iniciar sesión ahora mismo.” Incluye:
- Personas con IPs dinámicas (internet doméstico, viajes, hotspot móvil).
- Automatización legítima: sondas de uptime, publicación sin cabeza, clientes API de WooCommerce, callbacks de SSO, comprobaciones de salud de proxy inverso.
- Flujos de trabajo de editores con múltiples roles: administradores, autores, colaboradores.
- Acceso de emergencia cuando tu WAF o servicio de autenticación está degradado.
El endurecimiento debe ser reversible, observable y en capas. Si un cambio puede dejar fuera a tu propio personal, debe tener un plan de acceso alternativo (preferiblemente del tipo que puedas ejecutar a las 3 AM con un ojo abierto y sin heroísmos).
Una cita de confiabilidad que vale la pena pegar en el monitor: “La esperanza no es una estrategia.”
— Gordon R. Sullivan.
Datos interesantes y breve historia (lo que explica el desorden actual)
- WordPress comenzó en 2003 como un fork de b2/cafelog; su arquitectura de plugins le ayudó a ganar, y también dio a los atacantes una enorme superficie de ataque.
- xmlrpc.php precede a las APIs REST; permitía publicación remota mucho antes de que los patrones modernos de autenticación fueran comunes en CMS.
- La fuerza bruta pasó de “dirigida” a “ambiental” una vez que las botnets y los dumps de credenciales filtradas se abarataron; no te atacan por ser especial, te atacan por existir.
- Las actualizaciones automáticas del núcleo (para versiones menores) se convirtieron con el tiempo en una red de seguridad mainstream, pero los plugins siguen siendo el riesgo dominante.
- Los “plugins de seguridad” surgieron como categoría en gran parte porque el hosting compartido dificultaba el acceso a controles a nivel de servidor para propietarios normales.
- Los salts en wp-config.php existen porque las cookies y sesiones necesitan unicidad criptográfica por sitio; los sitios antiguos a veces arrastran salts antiguos tras migraciones.
- La guía de permisos de archivos evolucionó porque las primeras instalaciones de WordPress solían ejecutar PHP como el mismo usuario propietario de los archivos, lo que fomentaba directorios escribibles por todos.
- Los CDN cambiaron la historia del login: el rate limiting y la mitigación de bots se movieron hacia afuera, pero la mala configuración del origen aún filtra acceso directo a wp-login.
Principios de endurecimiento que previenen bloqueos
1) Reduce la superficie de ataque antes de aumentar la aplicación de controles
Bloquear al mundo desde /wp-admin se siente satisfactorio. También rompe a los editores móviles, clientes REST y a cualquiera cuya IP cambie. En su lugar, comienza eliminando lo que no usas (xmlrpc, plugins sin uso, temas antiguos), luego añade autenticación y limitación de tasa, y después agrega reglas de “deny” donde sean seguras.
2) Cada control de seguridad necesita observabilidad
Si no puedes responder “¿quién fue bloqueado?” y “¿por qué?” desde los logs, no estás haciendo seguridad. Estás haciendo ritual. Registra las decisiones de denegación con campos suficientes para actuar: IP, ruta, estado, user agent, id de petición y tiempo de respuesta upstream.
3) Prefiere límites de tasa y retos sobre listas de permitidos por IP
Las listas de permitidos funcionan para redes de oficina y VPNs. Fallan en la realidad remota/híbrida. La limitación de tasa + autenticación fuerte te da protección sin suposiciones frágiles sobre la estabilidad de IP.
4) Separa identidad de autorización
La autenticación multifactor (o SSO) fortalece la identidad. No reemplaza la autorización. Mantén los roles mínimos. No des “Administrador” para resolver fricción de flujo de trabajo. Eso es como añadir más gasolina porque el motor hace ruido.
5) Mantén pequeño el radio de impacto
Ejecuta PHP-FPM con un usuario dedicado. Bloquea la propiedad de los archivos. Si una vulnerabilidad de plugin golpea, tu objetivo es “daño limitado” en lugar de “todo el sistema de archivos es un lienzo”.
Broma #1: Lo único más persistente que el malware de WordPress es la persona que insiste en que la contraseña de admin debe ser “CompanyName2024!”.
Guía de diagnóstico rápido: encuentra el cuello de botella antes de “arreglarlo”
Cuando aparecen problemas de inicio de sesión tras el “endurecimiento”, trátalo como un incidente. No adivines. No reviertas a ciegas. Triunfa en este orden porque reduce rápidamente la búsqueda y evita que culpes la capa equivocada.
Primero: confirma qué está fallando (navegador vs servidor vs upstream)
- ¿Devuelve
/wp-login.phpun 200 con un formulario, o 403/429/503? - ¿La falla es solo para algunos usuarios (por rol, IP, geolocalización)?
- ¿Se está golpeando el origen directamente, omitiendo CDN/WAF?
Segundo: revisa decisiones de rate limiting / WAF / Fail2ban
- Busca picos de 403/429 en endpoints de login.
- Confirma si tus listas de bloqueo incluyen la egress de la oficina/VPN o tus IPs de monitoreo.
- Valida que un “reto” no esté rompiendo clientes no navegador (llamadas REST, apps móviles).
Tercero: revisa rendimiento de la app y la base de datos
- El login puede ser lento porque la base de datos está lenta (bloat en la tabla options, opciones autoloaded, índices faltantes).
- O porque los workers de PHP están saturados (muy pocos children en PHP-FPM, plugins lentos en exceso).
- O porque el disco está triste (picos de IO wait; logs y uploads en el mismo volumen).
Cuarto: revisa controles de corrección
- Permisos/propiedad que impiden escrituras de sesión, subidas, actualizaciones de plugins.
- Mala configuración del cache de objetos que causa bucles con cookies de autenticación.
- Cabeceras de seguridad o flags de cookies mal configurados entre HTTP/HTTPS.
Si no puedes identificar el cuello de botella en 10–15 minutos, congela cambios, toma snapshot del estado (configs + logs) y procede con rollback controlado. La adrenalina es un pésimo sistema de gestión de cambios.
Tareas prácticas de endurecimiento (comandos, salidas, decisiones)
Estas son tareas operativas reales que puedes ejecutar en un host Linux típico (variante Debian/Ubuntu). Cada tarea incluye: el comando, cómo se ve una salida “buena” y qué decisión tomar.
Task 1: Identify who is actually serving WordPress (Nginx/Apache, PHP-FPM user)
cr0x@server:~$ ps -eo user,comm,args | egrep 'nginx:|apache2|php-fpm' | head
root nginx: nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
www-data nginx: nginx: worker process
www-data php-fpm8.2 php-fpm: pool www
www-data php-fpm8.2 php-fpm: pool www
Qué significa: Los workers de Nginx y PHP-FPM se ejecutan como www-data. Eso informa las elecciones de propiedad y permisos de archivos.
Decisión: Estandariza en un usuario de servicio dedicado (www-data está bien) y asegura que los archivos de WordPress pertenezcan a un grupo controlado por administradores, no escribibles por el usuario web a menos que quieras actualizaciones in-place.
Task 2: Confirm WordPress path and detect accidental multiple installs
cr0x@server:~$ sudo find /var/www -maxdepth 3 -name wp-config.php -print
/var/www/site1/wp-config.php
Qué significa: Solo se encontró un wp-config.php bajo las raíces web esperadas. Múltiples configs suelen significar sitios staging olvidados expuestos a internet.
Decisión: Si encuentras instalaciones extra, elimínalas, ponlas detrás de un firewall o protégelas con contraseña. Los sitios staging ocultos son donde los atacantes van de compras.
Task 3: Check file ownership and writable surfaces
cr0x@server:~$ cd /var/www/site1
cr0x@server:~$ sudo find . -maxdepth 2 -type d -perm -0002 -print | head
./wp-content/uploads
Qué significa: Directorios escribibles por “otros” son señales de alerta. WordPress necesita que uploads sea escribible, pero no necesariamente world-writable.
Decisión: Elimina los bits world-writable. Mantén solo lo que deba ser escribible por el usuario PHP (normalmente wp-content/uploads y quizás directorios de cache).
Task 4: Apply sane permissions (without breaking media uploads)
cr0x@server:~$ sudo chown -R root:www-data /var/www/site1
cr0x@server:~$ sudo find /var/www/site1 -type d -exec chmod 0750 {} \;
cr0x@server:~$ sudo find /var/www/site1 -type f -exec chmod 0640 {} \;
cr0x@server:~$ sudo chown -R www-data:www-data /var/www/site1/wp-content/uploads
cr0x@server:~$ sudo find /var/www/site1/wp-content/uploads -type d -exec chmod 0750 {} \;
cr0x@server:~$ sudo find /var/www/site1/wp-content/uploads -type f -exec chmod 0640 {} \;
Qué significa: Los archivos core no son escribibles por el usuario en tiempo de ejecución; los uploads sí lo son. Esto bloquea una gran clase de persistencia “subir webshell al core”.
Decisión: Si dependes de actualizaciones desde el dashboard para plugins/temas, acepta un path escribible controlado (y monitoreo fuerte), o cambia a actualizaciones basadas en despliegue (recomendado en entornos serios).
Task 5: Lock down wp-config.php specifically
cr0x@server:~$ sudo stat -c '%a %U:%G %n' /var/www/site1/wp-config.php
640 root:www-data /var/www/site1/wp-config.php
Qué significa: El usuario web (en el grupo www-data) puede leer la config, pero no escribirla. Eso suele ser lo que quieres.
Decisión: Si el archivo es escribible por www-data, arréglalo. Si el servidor web no puede leerlo, tu sitio no arrancará. No “soluciones” con 777.
Task 6: Verify core and plugin integrity with WP-CLI
cr0x@server:~$ cd /var/www/site1
cr0x@server:~$ sudo -u www-data wp core verify-checksums
Success: WordPress installation verifies against checksums.
Qué significa: Los archivos core coinciden con los checksums esperados. Si esto falla, puede haber manipulación o estás en una versión/build que no coincide con los checksums.
Decisión: Las fallas requieren investigación. Si no parcheaste intencionalmente el core, trata la discrepancia como sospechosa y reinstala el core desde una fuente de confianza.
Task 7: Enumerate plugins and kill what you don’t use
cr0x@server:~$ sudo -u www-data wp plugin list --status=inactive
+---------------------+----------+-----------+---------+
| name | status | update | version |
+---------------------+----------+-----------+---------+
| hello-dolly | inactive | none | 1.7.2 |
| old-seo-plugin | inactive | available | 2.1.0 |
+---------------------+----------+-----------+---------+
Qué significa: Los plugins inactivos siguen siendo código en disco. Las vulnerabilidades no distinguen si tu checkbox de UI está apagado.
Decisión: Desinstala plugins inactivos a menos que tengas una razón contundente para mantenerlos (y aun así, conserva un artefacto empaquetado en lugar de código en vivo).
Task 8: Update safely with a “what will change” preview
cr0x@server:~$ sudo -u www-data wp plugin update --all --dry-run
Available plugin updates:
- woocommerce (7.9.0 -> 7.9.2)
- wordfence (7.10.1 -> 7.10.2)
Success: Checked available updates.
Qué significa: Puedes ver el radio de impacto antes de aplicar cambios. Los dry runs están infrautilizados porque a la gente aparentemente le gustan las sorpresas.
Decisión: Escena actualizaciones, especialmente en sitios con WooCommerce o que generan ingresos. Avanza con backups y un plan de rollback.
Task 9: Detect if xmlrpc.php is still reachable (and decide what to do)
cr0x@server:~$ curl -s -o /dev/null -w '%{http_code}\n' https://example.com/xmlrpc.php
200
Qué significa: xmlrpc.php es accesible. Eso no es automáticamente malo, pero suele ser abusado.
Decisión: Si no usas Jetpack, publicación móvil antigua o pingbacks, bloquéalo. Si lo usas, restringe los métodos o limita la tasa agresivamente.
Task 10: Block xmlrpc.php at the web server (Nginx) without touching wp-login
Snippet de ejemplo para Nginx (aplica en el server block):
cr0x@server:~$ sudo nginx -T 2>/dev/null | sed -n '1,120p'
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Qué significa: La sintaxis es válida. Ahora añade un location explícito (mostrado abajo) y vuelve a probar.
cr0x@server:~$ sudo tee /etc/nginx/snippets/wordpress-xmlrpc-block.conf >/dev/null <<'EOF'
location = /xmlrpc.php {
deny all;
access_log /var/log/nginx/xmlrpc-block.log;
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
cr0x@server:~$ sudo systemctl reload nginx
Decisión: Si más tarde descubres una dependencia de negocio en XML-RPC, revierte este snippet y pasa a limitar la tasa en lugar de denegar por completo.
Task 11: Rate-limit wp-login.php and wp-admin endpoints (throttle, don’t brick)
Los límites de tasa deben dirigirse a patrones abusivos, no al trabajo editorial normal. Mantén límites modestos y monitorea.
cr0x@server:~$ sudo tee /etc/nginx/snippets/wordpress-login-ratelimit.conf >/dev/null <<'EOF'
limit_req_zone $binary_remote_addr zone=wp_login:10m rate=10r/m;
location = /wp-login.php {
limit_req zone=wp_login burst=20 nodelay;
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
}
location ~* ^/wp-admin/ {
limit_req zone=wp_login burst=40 nodelay;
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: Estás aplicando limitación por IP a rutas de login/admin. El burst permite picos cortos legítimos (cargas de página) sin castigar a humanos.
Decisión: Si ves muchos 429s de usuarios reales, afloja el rate o aumenta el burst. Si ves ataques sostenidos, aprieta y añade Fail2ban encima.
Task 12: Confirm headers and cookie behavior aren’t sabotaging logins
cr0x@server:~$ curl -I https://example.com/wp-login.php | egrep -i 'set-cookie|strict-transport|content-security|x-frame|x-content-type'
strict-transport-security: max-age=31536000; includeSubDomains
x-frame-options: SAMEORIGIN
x-content-type-options: nosniff
Qué significa: Tienes cabeceras de seguridad base. Cabeceras faltantes no siempre rompen el login, pero flags de cookies mal configurados entre HTTP/HTTPS sí pueden.
Decisión: Si los inicios de sesión hacen bucle o las cookies no persisten, valida la terminación TLS, WP_HOME/WP_SITEURL y las cabeceras del proxy (ver errores comunes).
Task 13: Check whether wp-login is being hammered (access logs)
cr0x@server:~$ sudo awk '$7 ~ /wp-login.php/ {print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head
842 203.0.113.50
611 198.51.100.24
402 192.0.2.10
Qué significa: IPs fuente principales golpeando el login. Grandes conteos sugieren fuerza bruta o credential stuffing.
Decisión: Si pocas IPs dominan, Fail2ban puede ayudar. Si miles de IPs distribuyen la carga, prefiere desafíos CDN/WAF y límites de tasa.
Task 14: Install and validate Fail2ban for WordPress (Nginx example)
cr0x@server:~$ sudo apt-get update
Hit:1 http://archive.ubuntu.com/ubuntu jammy InRelease
Reading package lists... Done
cr0x@server:~$ sudo apt-get install -y fail2ban
Setting up fail2ban (0.11.2-6) ...
Crea un filtro que coincida con inicios de sesión fallidos repetidos (necesitarás un log_format que incluya ruta y estado; adáptalo a tu formato). Ejemplo de jail:
cr0x@server:~$ sudo tee /etc/fail2ban/jail.d/wordpress-login.conf >/dev/null <<'EOF'
[wordpress-login]
enabled = true
port = http,https
filter = wordpress-login
logpath = /var/log/nginx/access.log
findtime = 600
bantime = 3600
maxretry = 20
EOF
cr0x@server:~$ sudo tee /etc/fail2ban/filter.d/wordpress-login.conf >/dev/null <<'EOF'
[Definition]
failregex = ^<HOST> .* "(GET|POST) /wp-login\.php.*" (200|401|403|404) .*
ignoreregex =
EOF
cr0x@server:~$ sudo systemctl restart fail2ban
cr0x@server:~$ sudo fail2ban-client status wordpress-login
Status for the jail: wordpress-login
|- Filter
| |- Currently failed: 0
| |- Total failed: 0
| `- File list: /var/log/nginx/access.log
`- Actions
|- Currently banned: 0
|- Total banned: 0
`- Banned IP list:
Qué significa: La jail está activa. “Currently failed” incrementa cuando el regex coincide. “Currently banned” muestra los bloqueos activos.
Decisión: Ajusta failregex a tu formato real de access log. No despliegues reglas de Fail2ban sin probarlas, a menos que disfrutes bloquear el Wi‑Fi del CEO en un hotel.
Task 15: Verify TLS termination and proxy headers (a frequent login loop culprit)
cr0x@server:~$ sudo -u www-data wp option get siteurl
https://example.com
cr0x@server:~$ sudo -u www-data wp option get home
https://example.com
Qué significa: WordPress piensa que usa HTTPS. Si terminas TLS en un load balancer y reenvías HTTP al origen, también debes reenviar X-Forwarded-Proto y configurar WordPress acorde.
Decisión: Si estas opciones son http:// mientras los usuarios usan https://, arréglalas y asegura que tu proxy inverso establezca X-Forwarded-Proto https.
Task 16: Check PHP-FPM saturation (login failures under load)
cr0x@server:~$ sudo tail -n 20 /var/log/php8.2-fpm.log
[04-Feb-2026 03:12:41] WARNING: [pool www] server reached pm.max_children setting (10), consider raising it
Qué significa: Los workers de PHP están agotados. Bajo ataque, esto parece “inicios de sesión rotos”, pero es agotamiento de recursos.
Decisión: Aumenta pm.max_children solo después de confirmar margen de CPU/RAM. También reduce trabajo por petición (cache, limitar plugins costosos, añadir WAF/límites de tasa).
Task 17: Confirm database health (slow auth queries can look like security blocks)
cr0x@server:~$ mysql -e "SHOW PROCESSLIST\G" | sed -n '1,40p'
*************************** 1. row ***************************
Id: 41
User: wpuser
Host: 127.0.0.1:48822
db: wordpress
Command: Query
Time: 12
State: Sending data
Info: SELECT option_name, option_value FROM wp_options WHERE autoload = 'yes'
Qué significa: La consulta de opciones autoloaded está tardando. Esto suele inflarse con los años cuando plugins almacenan basura en options.
Decisión: Audita el tamaño de autoload y reduce lo necesario. El endurecimiento no ayuda si el endpoint de login pasa segundos transportando un contenedor de opciones en cada petición.
Task 18: Check disk pressure (because logins need IO too)
cr0x@server:~$ df -h /var/www /var/log | tail -n 2
/dev/sda1 80G 74G 2.1G 98% /
/dev/sda1 80G 74G 2.1G 98% /
Qué significa: El sistema de archivos está casi lleno. Esto rompe sesiones, subidas, registro, actualizaciones y a veces escrituras de base de datos. También hace más ruidosas a todas las herramientas de seguridad.
Decisión: Libera espacio inmediatamente. Luego separa logs/uploads del filesystem raíz y añade alertas antes de que llegue al 90%.
Broma #2: Disco al 98% es la forma en que el servidor dice “No estoy enfadado, solo estoy decepcionado”.
Tres microhistorias corporativas desde las trincheras
Incidente #1: Una suposición equivocada (allowlisting por IP “por seguridad”) provocó una espiral de bloqueos
Una empresa mediana gestionaba un sitio WordPress para contenido de soporte y captación de leads. Tras una alarma leve—algunas entradas sospechosas en los logs—un ingeniero hizo lo que muchos hemos hecho bajo presión: bloquearon /wp-admin y /wp-login.php al rango de IPs de la oficina corporativa. Funcionó al instante. El ruido de login paró. El dashboard se sintió en paz.
Luego el equipo de ventas volvió a la ruta. Wi‑Fi doméstico, aeropuertos, hoteles, tethering móvil. De repente, los informes de “WordPress no funciona” se dispararon, porque desde su perspectiva, así era. El equipo de ingeniería supuso que era error del usuario, luego supuso que era la VPN, luego asumió que era el IdP. Mientras tanto, el equipo de marketing ops comenzó a compartir una cuenta admin “solo para poder trabajar”, porque solo una persona seguía teniendo acceso.
El modo de fallo empeoró: una agencia externa que gestionaba SEO ya no podía acceder. Pidieron acceso; alguien abrió temporalmente la allowlist a “cualquiera”, se olvidó de quitarla y el endpoint de login volvió a ser martillado. El equipo acabó con lo peor de ambos mundos: bloqueos para usuarios reales y exposición a la amenaza original.
La solución no fue heroica. Reemplazaron el allowlisting por controles en capas: contraseñas fuertes + 2FA, un rate limit modesto en Nginx para wp-login.php y Fail2ban para intentos repetidos. También aseguraron que el origen no fuera accesible excepto a través del CDN/WAF. El acceso se estabilizó y el ruido del ataque cayó a un zumbido manejable y observable.
La lección: las listas de permitidos por IP no son “seguridad”. Son un sistema de identidad frágil. Úsalas para paneles de administración de infraestructura y staging privado. Para logins de WordPress en 2026, por defecto usa throttling y autenticación fuerte.
Incidente #2: Una optimización que salió mal (caché agresivo rompió la autenticación)
Otra organización ejecutaba WordPress detrás de Nginx con una capa de caché. El rendimiento era una métrica clave; el equipo cuidaba los tiempos de carga más que la mayoría cuida los fines de semana. Un ingeniero añadió reglas de caché para acelerar páginas dinámicas. La tasa de acierto subió. La CPU bajó. Aplausos por todos lados.
En horas, aparecieron tickets de soporte: los usuarios no podían iniciar sesión, o iniciaban sesión y eran desconectados inmediatamente. Algunos veían la barra de admin de otros usuarios parpadear brevemente—raro, pero terrorífico. El equipo pensó inicialmente en XSS o robo de sesiones. El pánico es ruidoso.
El verdadero culpable fue aburrido: la capa de caché almacenó respuestas que no debía. Algunas páginas se sirvieron con cookies inapropiadas y la caché no variaba correctamente según el estado de autenticación. La optimización era correcta para contenido anónimo y catastrófica para cualquier cosa relacionada con sesiones.
La recuperación fue una reescritura cuidadosa de reglas de caché: nunca cachear /wp-login.php, nunca cachear /wp-admin y evitar cachear páginas cuando hay cookies de autenticación presentes. También añadieron condiciones explícitas de bypass para cookies de WooCommerce y de membresía. Después de eso, la estabilidad de los inicios de sesión volvió y las ganancias de rendimiento se mantuvieron—solo no en los endpoints donde la corrección es innegociable.
La lección: afinar rendimiento puede convertirse en un incidente de seguridad si rompe el aislamiento. Cachea como adulto: define lo que no debe cachearse y verifica con pruebas y cabeceras.
Incidente #3: Una práctica aburrida pero correcta salvó el día (mínimos privilegios + simulacros de restauración)
Una compañía SaaS alojaba un sitio WordPress de marketing que parecía simple pero tenía valor de negocio serio: páginas de precios, flujos de registro e historias de clientes usadas en acuerdos empresariales. Lo trataron como producción, porque lo era. El equipo tenía una política que sonaba aburrida: propiedad de archivos bloqueada, sin instalaciones de plugins in-place en producción y simulacros mensuales de restauración de base de datos y uploads.
Un fin de semana, salió a la luz una vulnerabilidad de un plugin. Los bots se movieron rápido. Su sitio comenzó a recibir peticiones maliciosas apuntando al endpoint de subida de ese plugin. Algunas peticiones llegaron a PHP, pero el exploit falló en persistir porque el usuario web no podía escribir en directorios core o de plugins. Los atacantes pudieron hurgar; no pudieron montar campamento.
Aun así, no declararon victoria. Supusieron “los intentos funcionaron en algún lado”. Ejecutaron comprobaciones de integridad, compararon directorios de plugins contra artefactos conocidos buenos e inspeccionaron logs por POSTs anómalos. Rotaron keys/salts y forzaron restablecimientos de contraseña para admins por precaución.
La verdadera victoria vino de la práctica aburrida: el equipo tenía un procedimiento de restauración probado. Tomaron un snapshot, restauraron en un entorno aislado y validaron el comportamiento del sitio y el árbol de archivos. Esta vez no fue necesario para recuperar, pero convirtió el miedo en un paso de verificación controlado. Sin adivinanzas, sin superstición.
La lección: cuando puedes restaurar de forma fiable, puedes endurecer agresivamente. No porque planees fallar, sino porque te niegas a quedar atrapado por el fracaso.
Errores comunes: síntomas → causa raíz → solución
La página de login devuelve 403 para todos
Síntomas: Todos los usuarios ven 403 en /wp-login.php. Acceso admin muerto. Editores gritando.
Causa raíz: Regla de deny demasiado amplia (Nginx/Apache), regla WAF configurada en “block” en lugar de “challenge”, o restricción geográfica/IP aplicada al endpoint de login.
Solución: Revierte la regla dirigiéndola solo a /xmlrpc.php o rutas conocidas malas; cambia a rate limiting; añade una vía de bypass de emergencia accesible solo desde VPN.
Los usuarios pueden cargar wp-login, pero las credenciales nunca permanecen (bucle de login)
Síntomas: El usuario envía la contraseña correcta y es redirigido de vuelta al login. Las cookies no persisten.
Causa raíz: Desajuste en la terminación HTTPS (siteurl/home erróneos), falta X-Forwarded-Proto o cacheo de respuestas de login.
Solución: Corrige home y siteurl a HTTPS, configura las cabeceras del proxy, desactiva el cache para endpoints de autenticación, verifica COOKIE_DOMAIN y el host canónico.
Algunos usuarios reciben 429 Too Many Requests mientras otros están bien
Síntomas: Equipos remotos se quejan; usuarios de oficina están bien. La app móvil falla intermitentemente.
Causa raíz: Límite de tasa mal claveado (por ejemplo, por una IP NAT compartida, o por una cabecera que colapsa usuarios distintos en un solo bucket).
Solución: Clavea los límites por $binary_remote_addr en el borde; si estás detrás de un proxy, asegúrate de que la IP real del cliente esté correctamente establecida (módulo real_ip). Aumenta el burst. Añade CAPTCHA/retos solo donde se esperan humanos.
Páginas admin lentas, logins expiran bajo carga
Síntomas: Errores 504/502, fallos intermitentes en login, CPU e IO wait suben.
Causa raíz: Saturación de PHP-FPM, consultas lentas en la BD o disco casi lleno. A veces un ataque de fuerza bruta solo amplifica ineficiencias subyacentes.
Solución: Incrementa capacidad de PHP-FPM si hay recursos, añade caché correctamente, optimiza la base de datos, reduce opciones autoloaded y añade protección upstream (WAF/límites de tasa).
Tras el “endurecimiento”, fallan actualizaciones de plugins y subidas de medios
Síntomas: “Could not create directory,” fallos de actualización, errores de permisos.
Causa raíz: Propiedad/permisos demasiado estrictos o inconsistentes entre webroot y wp-content.
Solución: Decide: actualizaciones basadas en despliegue (preferido) o permite acceso de escritura controlado a wp-content. No hagas core escribible para resolver un flujo de trabajo de plugin.
Fail2ban bloquea repetidamente a usuarios reales
Síntomas: Usuarios bloqueados después de pocos intentos; los baneos se correlacionan con NAT de oficina/VPN.
Causa raíz: Regex demasiado amplia, maxretry muy bajo, NAT concentra muchos usuarios detrás de una IP.
Solución: Incrementa maxretry, acorta bantime, afina el regex a fallos reales (por ejemplo, 200 en wp-login no siempre es “fallo”) y prefiere desafíos WAF cuando sea posible.
Listas de verificación / plan paso a paso (no improvises en prod)
Fase 0: Prevuelo (haz esto antes de tocar controles de seguridad)
- Inventario de dependencias: ¿Usas XML-RPC? ¿Jetpack? ¿Publicación móvil? ¿Sistemas externos que golpean endpoints REST?
- Decide dónde reside la aplicación: CDN/WAF primero, luego servidor web, luego plugins de la app. No pongas todo dentro de WordPress si puedes evitarlo.
- Establece acceso de emergencia: Una vía VPN, un bastión o un procedimiento temporal de mantenimiento que no dependa del login de WordPress.
- Backups + prueba de restauración: Verifica que puedes restaurar la base de datos y
wp-content/uploadsen un entorno limpio. - Ventana de cambios y rollback: Sabe exactamente qué archivos de configuración y reglas vas a cambiar y cómo revertir rápido.
Fase 1: Elimina ganancias fáciles (riesgo bajo, mayor retorno)
- Elimina plugins y temas sin usar. Inactivo no es seguro; es código inactivo.
- Actualiza el core y plugins de WordPress. Haz staging primero si el sitio es crítico para ingresos.
- Establece permisos y propiedad correctos. Mantén el core de solo lectura para el runtime; mantén uploads escribibles.
- Regenera salts/keys si sospechas compromiso o si el sitio ha pasado por múltiples manos en migraciones.
Fase 2: Protege el login sin romperlo
- Limita la tasa de
/wp-login.phpen el servidor web o en el edge. Evita denegaciones duras a menos que estés seguro. - Habilita 2FA para administradores (o SSO con políticas fuertes). Es el reductor de riesgo más grande específico para login.
- Deshabilita XML-RPC si no se usa; de lo contrario, restringe y monitorea.
- Limita las vías de creación de admins (no permitas que plugins otorguen escalado de roles sin auditoría; revisa usuarios regularmente).
Fase 3: Añade salvaguardas y visibilidad
- Centraliza logs (al menos access + error de Nginx/Apache, logs de PHP-FPM y logs de app relevantes para autenticación).
- Alerta sobre anomalías: picos en peticiones a
/wp-login.php, aumentos de 403/429, uso de disco, saturación de PHP-FPM, tasas de consultas lentas en BD. - Monitoreo de integridad de archivos para directorios core y plugins (hashes o checksums). Si cambia fuera de despliegues, eso es alarma.
- Practica la respuesta a incidentes: aislar origen, rotar credenciales, restaurar en ambiente limpio y verificar.
Lista de “No hagas esto” (porque te sentirás tentado)
- No ocultes
/wp-admincon plugins de seguridad basados en oscuridad y lo des por “hecho.” Los bots rastrean y la gente olvida. - No pongas permisos 777 para “arreglar” actualizaciones. Eso no arregla; es rendirse.
- No confíes en allowlists de IP para usuarios que viajan. Así creas cuentas admin en la sombra.
- No despliegues reglas de caché no probadas en endpoints de autenticación. Si debes hacerlo, prueba con múltiples sesiones y cookies.
Preguntas frecuentes
1) ¿Debería deshabilitar xmlrpc.php?
Si no usas características que dependen de él, sí—bloquéalo en el servidor web. Si lo usas (Jetpack, alguna publicación legacy), mantenlo pero aplica throttling y monitoreo.
2) ¿Cambiar la URL de login es un control de seguridad real?
Es reducción de ruido, no un control primario. Puede reducir bots tontos, pero no detendrá ataques dirigidos o plugins explotados. Úsalo solo si no rompe integraciones y mantienes límites de tasa y autenticación fuerte.
3) ¿El rate limiting afectará a usuarios legítimos detrás de NAT?
Puede. Por eso estableces tasas y bursts razonables y claveas correctamente en la IP real del cliente. Empieza permisivo, vigila los 429 y luego ajusta.
4) ¿Fail2ban es suficiente sin WAF/CDN?
Fail2ban ayuda cuando los ataques se concentran en IPs repetidas. El credential stuffing suele repartirse en muchas IPs, donde las protecciones en el edge funcionan mejor. En la práctica: usa ambos si el sitio importa.
5) ¿Qué permisos de archivos debería tener WordPress?
El core debe ser de solo lectura para el runtime. Uploads deben ser escribibles. Un patrón común: core propiedad de root, grupo www-data, con archivos 0640 y directorios 0750; uploads propiedad de www-data.
6) ¿Debo permitir que WordPress actualice plugins desde el panel?
Para sitios personales, quizá. Para sitios de negocio en producción, prefiere actualizaciones basadas en despliegue para que el runtime web no pueda escribir código ejecutable. Si debes permitirlo, limita permisos a wp-content y monitorea la integridad.
7) ¿Por qué fallan los inicios de sesión después de habilitar una política de cabeceras de seguridad?
Una Content Security Policy (CSP) demasiado estricta puede bloquear scripts en la página de login, especialmente con temas personalizados o plugins que inyectan assets. Despliega CSP en modo reporte primero y luego ajusta.
8) ¿Cómo sé si el origen está expuesto detrás del CDN?
Revisa DNS y reglas de firewall. Si la IP de origen es alcanzable y sirve WordPress directamente, los atacantes pueden evitar tus controles en el edge. Bloquea el origen a rangos IP del CDN o usa una ruta de red privada.
9) ¿Necesito un plugin de seguridad?
No siempre. Los controles a nivel de servidor (rate limits, WAF, permisos, actualizaciones, monitoreo) hacen la mayor parte del trabajo pesado. Los plugins de seguridad pueden aportar 2FA y funciones convenientes, pero también son más código en tu app.
10) ¿Cuál es lo mínimo que debo hacer hoy si estoy abrumado?
Actualiza core/plugins, elimina plugins/temas no usados, bloquea permisos, añade rate limiting en /wp-login.php y habilita 2FA para administradores. Luego añade logging/alertas para ver qué está pasando.
Conclusión: siguientes pasos que realmente importan
Endurecer WordPress sin romper los inicios de sesión es mayormente cuestión de contención. No empuñes el martillo de prohibiciones primero. Reduce la superficie de ataque, bloquea lo escribible, regula el abuso y añade autenticación fuerte. Luego instrumenta todo para que tus controles de seguridad no se conviertan en la próxima interrupción.
Haz esto a continuación:
- Ejecuta los pasos de integridad e inventario (checksums con WP-CLI, lista de plugins, permisos).
- Implementa rate limiting en el servidor web para
/wp-login.phpy bloquea/xmlrpc.phpsi no se usa. - Activa 2FA para administradores (o aplica política de SSO) y audita cuentas admin.
- Verifica la corrección de proxy/TLS para evitar bucles de login.
- Añade alertas: picos 403/429, avisos de PHP-FPM max_children, uso de disco y tasas de peticiones de login.
- Programa un simulacro de restauración. No porque seas paranoico—porque te gusta dormir.