Proteger el acceso a WordPress por país y por red local (LAN) con Core-Admin


#1

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.

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 en logins correctos. Los intentos 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.
  • Cubre todas las vías de login: formulario wp-login.php, XML-RPC y REST con application
    passwords
    . Son precisamente los vectores que más se atacan.
  • Resolución GeoIP local mediante la base de datos de Core-Admin (no consulta servicios
    externos en cada login).
  • Las sesiones por cookie no se ven afectadas: quien ya entró desde un país permitido y se
    desplaza a otro país mantiene su sesión; solo se bloquean logins nuevos.

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:

  1. Enable country policy — activa/desactiva la política para este WordPress.
  2. 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 tokens LocalLan / LAN / PrivateIp
    si quieres permitir la red local.
  3. 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.”

A partir de ahí, el usuario legítimo puede usar el enlace de “He olvidado mi contraseña” o contactar
con el administrador.


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, ...

Ante un geo-bloqueo, 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 wordpress_login_geo_blocked (nivel warning) con el usuario, dominio, motivo
    e IP.

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

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).

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.


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 app-passwords).
  • Granularidad: país (ISO-3166) + tokens LAN + IPs extra.
  • Capa: dentro de PHP, en el filtro authenticate, solo en login correcto.
  • 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
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) PHP (authenticate) País + LAN + IPs

Diferencias de protección y cómo combinarlas

  • limit_admin_to_ips y limit_post_to_ips actú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_ips es 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_ips con las IPs de oficina
    (perímetro duro) + limit_admin_to_ips para 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 con extra_allowed_ips para alguna VPN concreta. Flexible sin tener
    que mantener listas de IPs.
  • Defensa en profundidad: combina ambas — limit_post_to_ips excluyendo (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 LAN explícitamente; de lo contrario el
    acceso interno quedará bloqueado por defecto.
  • Usa extra_allowed_ips para casos puntuales (una VPN en el extranjero) en lugar de añadir un país
    entero.
  • Revisa periódicamente los avisos wordpress_login_geo_blocked: te indican intentos de acceso con
    credenciales correctas desde países no esperados (señal de credenciales filtradas).
  • Recuerda el factor proxy/CDN: si el sitio está detrás de un proxy inverso sin mod_remoteip /
    X-Forwarded-For bien configurado, REMOTE_ADDR será 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