Introducción
Core-Admin permite limitar desde qué países se acepta el inicio de sesión en un WordPress
concreto. La idea es simple: si tu sitio solo lo gestionan personas que están en España, Argentina
e Italia, puedes indicar ES, AR, IT y cualquier intento de login desde otro país será rechazado
en el momento de autenticarse, aunque la contraseña sea correcta.
Desde la versión 1.6 del plugin WLT (noviembre de 2026) la misma política se aplica también a
todo lo que rodea al login: la recuperación de contraseña, las sesiones ya abiertas y la
API REST con contraseñas de aplicación. Además, el plugin protege todos los WordPress (tengan
o no política de países) frente a la enumeración de usuarios y avisa de los cambios sensibles
en las cuentas (nuevos administradores, cambios de email…). Lo detallamos más abajo.
Esta característica complementa a otros dos mecanismos que ya ofrecía la plataforma —la limitación
de acceso por IP (limit_admin_to_ips) y la limitación de peticiones POST por IP
(limit_post_to_ips)— y al sistema de detección de cuentas WordPress comprometidas. Más adelante
explicamos cómo se relacionan y cuándo conviene cada uno.
La protección es preventiva (bloquea el login, no se limita a avisar después) y está diseñada
con un principio de seguridad clave: fail-open. Ante cualquier duda (no hay política, fallo de
resolución GeoIP, dirección no resoluble…) se acepta el acceso, de forma que la política nunca
pueda dejar a nadie fuera por un problema técnico.
Cómo funciona
El control lo aplica el plugin Core-Admin Wordpress Login Tracker (WLT) que la plataforma
despliega en cada WordPress. El plugin se engancha al filtro authenticate de WordPress con la
máxima prioridad, de modo que ve el resultado final de cualquier intento de autenticación y
puede convertir un login correcto en un rechazo.
Puntos importantes del diseño:
-
Solo se geolocaliza cuando hace falta. Los intentos de login fallidos (la inmensa mayoría en
un ataque de fuerza bruta) no se geolocalizan: ya fallan por sí solos. Esto evita penalizar el
rendimiento en el camino caliente y solo añade una resolución GeoIP cuando alguien acierta las
credenciales, pide recuperar una contraseña o navega con una sesión abierta (en este último caso
el resultado se reutiliza durante 10 minutos por usuario e IP). -
Cubre todas las vías de login: formulario
wp-login.phpy XML-RPC (filtro
authenticate), y la API REST con contraseñas de aplicación. Esta última no pasa por el filtro
authenticatede WordPress, así que el plugin la controla con su propio gancho
(wp_authenticate_application_password_errors) desde la versión 1.6. -
Resolución GeoIP local mediante la base de datos de Core-Admin (no consulta servicios
externos en cada login).
Qué más queda cubierto (WLT 1.6)
| Evento | Qué hace la política desde un país/red no permitido |
|---|---|
Login (wp-login.php, XML-RPC) |
Rechaza el login aunque la contraseña sea correcta |
| API REST con contraseña de aplicación | Rechaza la petición (401) |
Solicitud de recuperación de contraseña (wp-login.php?action=lostpassword y “Mi cuenta” de WooCommerce) |
Rechaza la solicitud antes de generar la clave y de enviar el correo, exista o no el usuario |
| Uso de un enlace de recuperación (cambio de contraseña) | Rechaza el cambio de contraseña aunque el enlace sea válido |
| Sesión ya abierta (cookie de login) | La petición se atiende como visitante anónimo y wp-admin responde 403
|
La recuperación de contraseña es especialmente relevante: antes, cualquiera desde cualquier país
podía hacer que WordPress enviara al administrador un correo de “Alguien ha solicitado un
restablecimiento de la contraseña”. El enlace solo llega al buzón del administrador (el atacante no
gana nada), pero el correo inesperado suele interpretarse como un nuevo ataque y genera
incidencias. Con la política activa, esas solicitudes se cortan antes de enviar nada.
El control de sesiones cubre el caso de una cookie robada (por ejemplo, tras un compromiso del
equipo del administrador o del propio sitio): con ella se entra sin hacer login, así que el control
del login no llegaba a actuar. Efecto secundario a tener en cuenta: un usuario que entró desde un
país permitido y viaja a otro país pierde el acceso con esa sesión mientras esté allí (ver
“Recomendaciones”). La sesión no se destruye: al volver a un país permitido sigue funcionando.
Tampoco se comprueban las peticiones que el servidor se hace a sí mismo (WordPress las usa en el
editor de ficheros y en Salud del sitio).
Protección de la red local (LAN)
Además del filtrado por país, la política protege frente a accesos desde localhost y direcciones
de red interna (127.0.0.1, ::1, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, etc.).
El objetivo de seguridad es evitar que un atacante que haya conseguido ejecutar código en el propio
servidor (o en una máquina de la red interna) pueda hacer login “desde dentro” haciendo binding a
la IP local de la máquina, saltándose el filtro por país.
Comportamiento por defecto: cuando la política de países está activa, los accesos desde
direcciones locales/privadas se rechazan. Si necesitas permitir el acceso interno (por ejemplo,
administración desde la propia LAN o desde una VPN que sale por IP privada), añade a la lista de
países permitidos uno de estos tokens (no distinguen mayúsculas/minúsculas):
| Token | Efecto |
|---|---|
LocalLan |
Permite logins desde direcciones locales/privadas/loopback |
LAN |
Igual que LocalLan (alias) |
PrivateIp |
Igual que LocalLan (alias) |
Ejemplo de configuración que acepta Italia y acceso desde la red local:
IT, LAN
Administración desde la interfaz web
La política se configura por WordPress, en la pestaña Security de su ficha.
Seleccionamos el wordpress en cuestión:
Luego:
Campos disponibles:
- Enable country policy — activa/desactiva la política para este WordPress.
-
Allowed countries — lista de códigos de país ISO-3166 de 2 letras separados por espacios
o comas (ej.ES, AR, IT). Aquí también se añaden los tokensLocalLan/LAN/PrivateIp
si quieres permitir la red local. -
Country policy extra ips — lista opcional de IPs que siempre se permiten,
independientemente del país (útil para oficinas o VPN en el extranjero con IP pública fija).
Requisitos y validaciones
-
Requiere WLT activado. La política la aplica el plugin de login, así que al guardar con la
política activa pero sin WLT, la operación se rechaza indicándolo. -
Códigos válidos. Si activas la política sin ningún código de país válido, la operación se
rechaza (para no acabar, por un error tipográfico, con una política vacía que aceptaría todo). Los
códigos no reconocidos se ignoran y se notifica.
Dónde se guarda la configuración
La configuración es un fichero JSON desplegado junto al WordPress (no se guarda en base de datos),
que es la única fuente de verdad que lee el plugin:
wp-content/.core-admin-wp-country-policy.json
Contenido de ejemplo (IT permitido, LAN denegada por defecto):
{"enabled": true, "mode": "allow", "countries": ["IT"], "extra_allowed_ips": [], "local-lan-ips-allowed": false}
Este planteamiento es el mismo que sigue la limitación de IP de administración
(wp-admin/.htaccess): el fichero desplegado manda, la interfaz lo escribe y el listado lo vuelve
a leer.
Qué ve el usuario bloqueado
Cuando se rechaza un login, WordPress muestra el error correspondiente en la pantalla de acceso:
- Bloqueo por país: “Access denied: logins to this site are not allowed from your country.”
- Bloqueo por red local: “Access denied: logins to this site are not allowed from your network.”
En la recuperación de contraseña (solicitud o uso del enlace):
- “Access denied: password recovery for this site is not allowed from your country.”
- “Access denied: password recovery for this site is not allowed from your network.”
Con una sesión abierta, el sitio se muestra como a un visitante anónimo y al entrar en wp-admin se
obtiene un 403 con “Access denied: this session is not allowed from your country or network.”
Como la recuperación de contraseña también queda bloqueada desde fuera, el usuario legítimo que se
encuentre en un país no permitido debe contactar con el administrador, que puede añadir su IP en
“Country policy extra ips” (o su país, si procede).
Registros y eventos que genera
Toda decisión no trivial deja traza en syslog, de modo que siempre se puede ver por qué la
política actuó o por qué se ignoró (fail-open). Todas las líneas las emite el plugin con el formato
de bloque genérico de Core-Admin y el agente las recoge en syslog_handler.
Bloqueos (deny)
Login rechazado por país no permitido:
Login geo-blocked detected for username=usuario, country=ES, allowed-countries=IT, remote-addr=203.0.113.10, domain=ejemplo.com, service=Wordpress Login, ...
Login rechazado por acceso desde red local (país sintético local-lan):
Login geo-blocked detected for username=usuario, country=local-lan, allowed-countries=IT, remote-addr=10.2.57.2, domain=ejemplo.com, service=Wordpress Login, ...
Las mismas líneas, con otro prefijo, para el resto de eventos cubiertos:
App-password geo-blocked detected for username=usuario, country=US, allowed-countries=ES, remote-addr=203.0.113.10, ...
Session geo-blocked detected for username=usuario, country=US, allowed-countries=ES, remote-addr=203.0.113.10, ...
Lost-password geo-blocked detected for username=usuario, country=US, allowed-countries=ES, remote-addr=203.0.113.10, ...
Password-reset geo-blocked detected for username=usuario, country=US, allowed-countries=ES, remote-addr=203.0.113.10, ...
Ante un geo-bloqueo de login, contraseña de aplicación o sesión, el agente:
-
Registra el intento de forma duradera (no se rota la contraseña: un país equivocado no implica
que las credenciales estén comprometidas). -
Genera un aviso (nivel warning) con el usuario, dominio, motivo e IP:
wordpress_login_geo_blocked,wordpress_app_password_geo_blockedo
wordpress_session_geo_blocked. Las líneas de sesión se emiten como mucho una vez cada 10 minutos
por usuario e IP, para no generar un aviso por cada página visitada.
Los bloqueos de recuperación de contraseña (Lost-password / Password-reset) quedan solo en
syslog, para auditoría, sin aviso: suelen ser bots y avisar de cada uno sería ruido.
Accesos aceptados con la política “saltada” (skip / fail-open)
Para que quede claro que la política está activa pero se ignoró por diseño, los casos fail-open
también se registran:
Login country-policy skipped (reason=local-lan-allowed) for username=usuario, remote-addr=10.2.57.2, ...
Login country-policy skipped (reason=extra-allowed-ip) for username=usuario, remote-addr=203.0.113.10, ...
Login country-policy skipped (reason=geoip-unresolved) for username=usuario, geoip-ip=203.0.113.10, ...
Significado de cada reason:
| reason | Por qué se aceptó |
|---|---|
local-lan-allowed |
IP local/privada y la política tiene activado LAN/LocalLan/PrivateIp
|
extra-allowed-ip |
IP incluida en “Country policy extra ips” |
geoip-unresolved |
IP pública que no se pudo resolver (o exec no disponible): se acepta para no bloquear a nadie |
Estas líneas también aparecen con los prefijos App-password, Session, Lost-password y
Password-reset según el evento.
Cambios de configuración (auditoría)
Al guardar la política se emite un aviso de auditoría con quién, qué dominio y la configuración
aplicada:
wordpress_country_policy_updated: User [...] set country login policy for hosting [ejemplo.com] to enabled=yes, countries=[IT], local_lan_allowed=no, extra_ips=[]
Relación con el bloqueo por fuerza bruta
Conviene conocer esta interacción: cuando la política rechaza un login, WordPress lo convierte en un
fallo de autenticación, por lo que además de la línea Login geo-blocked detected se genera
también una línea Login failure detected. Esto significa que un geo-bloqueo cuenta como intento
fallido y, repetido desde una IP nueva, puede acabar provocando el bloqueo de esa IP a nivel de
firewall (kandadu). Es un efecto deseado (defensa extra contra atacantes persistentes).
Los bloqueos de recuperación de contraseña, de sesión y de contraseña de aplicación por REST no
generan Login failure detected, así que no cuentan para ese bloqueo por fuerza bruta.
El sistema es indulgente con las IPs que han autenticado correctamente hace poco, evitando bloquear
una IP legítima conocida:
Found IP 10.2.57.2 with login failures, but with proper auth previously (as much 12 hours ago)
Diagnóstico de resolución GeoIP
Para comprobar qué país resuelve Core-Admin para una IP (lo mismo que consulta el plugin), usa:
crad-geoip-lookup.pyc 203.0.113.10 --json
Salida (JSON, fácil de parsear):
{"ip": "203.0.113.10", "status": true, "country": "IT", "country_long": "Italy", "asn": "AS137", "organization": "..."}
Para una IP privada (10.x, 192.168.x…) o localhost, no se resuelve país: esa es la razón por la
que esos accesos caen en la regla LAN y no en la de país.
Protección frente a la enumeración de usuarios
Conocer un nombre de usuario válido es la mitad del trabajo de un atacante: le dice por dónde empezar
la fuerza bruta, a qué cuenta pedirle una recuperación de contraseña y, a menudo, da pistas de la
propia contraseña. WordPress, por defecto, revela los usuarios por varias vías.
Desde WLT 1.6 estas vías quedan cerradas para los visitantes no identificados, en todos los
WordPress con el plugin (no depende de la política de países):
| Vía de enumeración | Qué hace ahora |
|---|---|
/?author=1, /?author=2… (redirige a /author/<usuario>/) |
Responde 404 (solo con enlaces permanentes “bonitos”; con los enlaces simples es el enlace normal al archivo del autor y no se toca) |
API REST /wp-json/wp/v2/users (también ?rest_route= y _embed) |
Responde 401
|
oEmbed (/wp-json/oembed/1.0/embed) |
Ya no incluye el nombre ni la URL del autor |
Sitemap de usuarios (wp-sitemap-users-1.xml) |
Se elimina del sitemap |
| Mensajes de error del login (“usuario desconocido” frente a “contraseña incorrecta para X”) | Un único mensaje genérico: “el nombre de usuario o la contraseña no son correctos” |
Los usuarios identificados (editor de bloques, pantallas de administración) mantienen el
comportamiento normal de WordPress.
Limitaciones conocidas:
- Las URL de archivo de autor
/author/<nombre>/siguen mostrando el nombre corto del usuario
(user_nicename), que muchas veces coincide con el login. No se bloquean porque los temas enlazan a
ellas. Si te preocupa, cambia el “Nombre visible públicamente” y usa logins que no coincidan con el
nombre publicado. - El formulario de recuperación de contraseña sigue indicando si la cuenta no existe (desde un país
no permitido ya no se llega a ese punto). - Los temas o integraciones headless que lean
/wp-json/wp/v2/userssin identificarse dejarán de
poder mostrar los datos del autor.
Avisos de cambios sensibles en las cuentas
Tras un compromiso, lo habitual es que el atacante deje una puerta trasera para volver: un
administrador nuevo, un usuario existente al que se le sube el rol, un cambio de email del
administrador (para recibir los enlaces de recuperación) o el registro abierto con rol de
administrador por defecto. Muchas veces lo hace desde una webshell, sin pasar por ningún login,
así que no hay país que comprobar.
Por eso WLT 1.6 registra y avisa (no bloquea) de estos cambios en todos los WordPress:
Evento (event=) |
Cuándo |
|---|---|
admin-created |
Se crea un usuario con rol de administrador |
role-elevated |
Un usuario existente pasa a ser administrador |
super-admin-granted |
Se concede superadministrador (multisitio) |
admin-email-changed |
Cambia el email de un administrador |
admin-password-changed |
Cambia la contraseña de un administrador |
site-admin-email-changed |
Cambia el email de administración del sitio (Ajustes → Generales) |
registration-enabled |
Se abre el registro de usuarios |
default-role-administrator |
El rol por defecto de los nuevos usuarios pasa a ser administrador |
Línea de ejemplo en syslog:
WordPress user-event detected for username=wpsupport, event=admin-created, actor=none, details=id-7, remote-addr=203.0.113.10, domain=ejemplo.com, ...
El campo actor indica quién hizo el cambio: el usuario identificado, wp-cli, o none cuando no
había nadie identificado, que es justo el caso típico de una webshell. Cada evento genera un aviso
wordpress_user_event_<evento> (nivel warning, p. ej. wordpress_user_event_admin_created).
Un cambio legítimo (un administrador que cambia su contraseña) también genera aviso: basta con
confirmarlo con el cliente. Los cambios escritos directamente en la base de datos (por SQL) no pasan
por WordPress y no se detectan por esta vía.
Relación con otras protecciones de acceso
Core-Admin ofrece tres mecanismos complementarios. Es importante entender en qué capa actúa cada
uno y qué protege, porque combinados forman una defensa en profundidad.
1. Limitación de login/administración por IP (limit_admin_to_ips)
En la propia ficha del WordPress (pestaña Security) puedes restringir el acceso a /wp-admin a
una lista de IPs concretas. Se implementa con un wp-admin/.htaccess (order deny,allow) a nivel
de Apache.
- Protege: el área de administración.
- Granularidad: IP / red.
- Capa: servidor web (antes de PHP).
- Cuándo: cuando administras siempre desde IPs fijas conocidas.
2. Limitación de peticiones POST por IP (limit_post_to_ips)
En Webhosting Management (opciones del hosting) puedes restringir todas las peticiones POST
del hosting a una lista de IPs/redes/IPv6. Se implementa con mod_rewrite sobre REMOTE_ADDR
(compatible con mod_remoteip), devolviendo 403 a cualquier POST que no provenga de una IP
permitida:
RewriteEngine On
RewriteCond %{REQUEST_METHOD} =POST
RewriteCond %{REMOTE_ADDR} !^203\.0\.113\.10$
RewriteRule .* - [F]
Como bloquear todo POST puede ser demasiado, existe skip_limit_post_to_ips_urls, que permite
excluir rutas concretas (una por línea, sin host ni protocolo, p.ej. /wp-json/.../callback,
/checkout-webhook.php) para que esas URLs sí acepten POST desde cualquier IP — útil para
callbacks de pasarelas de pago, webhooks, etc.
- Protege: cualquier envío de datos (logins, comentarios, XML-RPC, formularios, subidas…).
- Granularidad: IP / red / IPv6.
- Capa: servidor web (antes de PHP) → es la barrera más “dura”.
- Cuándo: cuando los envíos legítimos del sitio provienen de IPs conocidas.
3. Política de login por país (esta característica)
-
Protege: el inicio de sesión (wp-login, XML-RPC, REST con contraseñas de aplicación), la
recuperación de contraseña y las sesiones ya abiertas. - Granularidad: país (ISO-3166) + tokens LAN + IPs extra.
-
Capa: dentro de PHP (ganchos de autenticación, recuperación de contraseña y sesión de
WordPress), solo cuando hay credenciales correctas, una solicitud de recuperación o una sesión. - Cuándo: cuando no puedes enumerar las IPs de quienes acceden, pero sí los países.
Tabla comparativa
| Característica | Qué limita | Capa | Granularidad | Permite navegar el sitio |
|---|---|---|---|---|
limit_admin_to_ips |
Acceso a /wp-admin
|
Apache (.htaccess) | IP / red | Sí |
limit_post_to_ips |
Todas las peticiones POST del hosting | Apache (mod_rewrite) | IP / red / IPv6 | Sí (GET) |
| Política por país | El login (todas las vías), la recuperación de contraseña y las sesiones | PHP (WordPress) | País + LAN + IPs | Sí |
Diferencias de protección y cómo combinarlas
-
limit_admin_to_ipsylimit_post_to_ipsactúan en Apache, antes de PHP: protegen incluso si
WordPress o un plugin tienen una vulnerabilidad en el flujo de login, porque la petición ni
siquiera llega a ejecutar código PHP. Son barreras de perímetro. - La política por país actúa dentro de PHP: es más “inteligente” (entiende de países y cubre
XML-RPC/REST), pero requiere que la petición llegue a WordPress. Es una puerta de login. - Para frenar fuerza bruta / DoS de POST en el perímetro,
limit_post_to_ipses lo más fuerte
(los POST de IPs no permitidas ni llegan a PHP). La política por país no frena que los
intentos lleguen a WordPress; de eso se encarga el seguimiento de fallos del WLT + kandadu.
Combinaciones recomendadas:
-
Sitio gestionado desde oficinas con IP fija:
limit_post_to_ipscon las IPs de oficina
(perímetro duro) +limit_admin_to_ipspara el panel. Máxima protección. -
Sitio gestionado desde ubicaciones variables pero dentro de unos países: política por país
(ES, AR, IT), opcionalmente conextra_allowed_ipspara alguna VPN concreta. Flexible sin tener
que mantener listas de IPs. -
Defensa en profundidad: combina ambas —
limit_post_to_ipsexcluyendo (skip_…_urls) las
rutas públicas necesarias, y la política por país como puerta adicional sobre el login.
Recomendaciones y buenas prácticas
- Mantén WLT activado; sin él la política no se aplica.
- Si administras desde la propia LAN del servidor, añade
LANexplícitamente; de lo contrario el
acceso interno quedará bloqueado por defecto. - Usa
extra_allowed_ipspara casos puntuales (una VPN en el extranjero) en lugar de añadir un país
entero. -
Viajes: si un administrador va a trabajar desde otro país, añade antes su país o su IP. Con la
política activa no podrá entrar, ni recuperar la contraseña, ni seguir usando una sesión abierta
en España. - Revisa periódicamente los avisos
wordpress_login_geo_blocked,
wordpress_app_password_geo_blockedywordpress_session_geo_blocked: indican intentos de acceso
con credenciales (o una sesión) válidas desde países no esperados, señal de credenciales filtradas
o de una cookie robada. - Trata los avisos
wordpress_user_event_admin_created,..._role_elevatedy
..._default_role_administratorconactor=nonecomo posible compromiso hasta confirmarlos. - Recuerda el factor proxy/CDN: si el sitio está detrás de un proxy inverso sin
mod_remoteip/
X-Forwarded-Forbien configurado,REMOTE_ADDRserá la IP del proxy y la geolocalización (y
limit_post_to_ips) no verán la IP real del cliente. Verifica que la IP real llega al servidor. - Activa 2FA en las cuentas con privilegios como capa adicional.
Resumen
| Acción | Dónde |
|---|---|
| Activar/configurar política por país | Ficha del WordPress → pestaña Security |
| Permitir red local | Añadir LAN (o LocalLan / PrivateIp) a los países |
| Permitir IPs concretas | Campo Country policy extra ips |
| Limitar acceso a wp-admin por IP |
limit_admin_to_ips (pestaña Security) |
| Limitar POST por IP |
limit_post_to_ips (Webhosting Management) |
| Diagnosticar país de una IP | crad-geoip-lookup.pyc <ip> --json |
| Ver bloqueos | syslog (Login geo-blocked detected) + aviso wordpress_login_geo_blocked
|
| Ver bloqueos de API REST y sesiones | syslog (App-password / Session geo-blocked detected) + avisos wordpress_app_password_geo_blocked / wordpress_session_geo_blocked
|
| Ver bloqueos de recuperación de contraseña | syslog (Lost-password / Password-reset geo-blocked detected), sin aviso |
| Ver cambios sensibles en cuentas | syslog (WordPress user-event detected) + avisos wordpress_user_event_<evento>
|
| Protección frente a enumeración de usuarios | Automática en todos los WordPress con WLT 1.6 |
| Actualizar el plugin a la última versión | Automático cada noche; manual con crad-wordpress-login-tracker.pyc --reconcile
|





