Etiquetas

, , ,

En Menéame teníamos un problema no solucionado correctamente. Cada vez sufrimos más ataques DoS voluntarios e involuntarios (bots mal programados, como los de Dreamule) que en el mejor de los casos nos ocasionan gastos innecesarios (ancho de banda e instancias adicionales) y en el peor sobrecargan tanto que impiden el acceso normal a las páginas. A esto se suma un gran problema: las instancias webs (que están autoescaladas horizontalmente) están detrás del balanceador de carga de Amazon AWS (ELB, Elastic Load Balancer) y éste no permite gestionar, ni manual ni automáticamente, el bloqueo de direcciones IP.

Por ello había que buscar una solución que detecte los DoS a conexiones que se hacen a las diferentes instancias web, y que sea el propio software de Menéame que lo haga y rechace las conexiones lo más rápido posible, y consumiendo los mínimos recursos. Esto obligaba a que se registren en un sólo servidor todas las conexiones desde las diferentes instancias, se analice en él en “tiempo real” y se guarden los datos en la base de datos para que estén disponibles para todas las instancias. Éstas deberían ser capaces de rechazar esas conexiones consumiendo la cantidad mínima de recursos y ancho de banda (tanto en la de Internet, como a la base de datos, como para enviar los logs al servidor central).

Después de pensarlo durante meses, y de analizar decenas de alternativas, en unos pocos días se me ocurrió la solución “óptima”. Esto ocurrió después de un mes de vacaciones casi completamente desconectado. Es el valor que tiene el descanso, pero eso es otro tema.

La arquitectura actual se usa el rsyslog para  enviar los registros desde cada instancia hacia el servidor. En el servidor se ejecuta un script (en Python) que analiza continuamente el fichero de logs, cada pocos segundos (actualmente, 6) calcula las estadísticas de acceso. Si alguna IP supera el umbral (15 conexiones/segundo) durante dos periodos seguidos es inmediatamente “bloqueada” insertando sus datos en la base de datos. Al inicio de cada conexión las instancias web verifican la IP en la base de datos (con un caché local para minimizar consultas), si está en la lista de bloqueadas la conexión es rechazada y cerrada inmediatamente.

Lo que se hace actualmente es:

1. Al software de Menéame le agregué una función de shutdown, que se ejecuta al final de todos los scripts que se ejecutan vía web (la función shutdown() al final del libs/init.php).

2. Este función prepara los datos que se registran (dirección IP, usuario si está registrado, tiempo de ejecución del script, nombre del script, y el dominio) y lo envía vía la función syslog() al rsyslog.

3. El rsyslog local de cada instancia filtra los mensajes que llegan desde el software de Menéame y lo envía al servidor central. Los mensajes son de dos tipos: de avisos y errores de la propia aplicación, y los registros de conexiones. A estos dos tipos de mensajes los envía (vía UDP para minimizar tráfico y latencia, no importa si de vez en cuando se pierde los datos de una conexión entre millones) hacia el servidor central vía el siguiente filtro en /etc/rsyslog.d/45-meneame.conf:

if $programname == 'meneame' or $syslogfacility-text == 'user' then @aws0.meneame.net
 & ~

4. Los datos son recibidos por el servidor central que dependiendo del tipo de log los separa en ficheros diferentes, uno para los avisos y alertas y el otro sólo para datos de conexiones. Esto se especifica en el /etc/rsyslog.d/45-meneame.conf del servidor:

# provides UDP syslog reception
$ModLoad imudp
$UDPServerRun 514

$template ReducedLog,"%timereported% %fromhost-ip% %msg%\n"

$RepeatedMsgReduction off
#Filtro para los registros de conexiones
if $programname == 'meneame' and $syslogpriority-text == 'debug' then /mnt/meneame_access.log;ReducedLog
& ~

$RepeatedMsgReduction on
#Filtro para los demás mensajes
if $syslogfacility-text == 'user' or $programname == 'meneame' then /var/log/meneame.log
& ~

El resultado es un fichero de conexiones con el siguiente formato:

Screenshot from 2013-09-11 18:08:15

5. En el mismo servidor central se ejecuta como daemon el pequeño programa en Python check_access que analiza continuamente el fichero de conexiones y cada 6 segundos obtiene las estadísticas y compara el número de conexiones por segundo. Si una IP supera el límite de conexiones por segundo (actualmente 15 en la media de los 6 segundos) almacena la IP en la base de datos de “noaccess” y nos notifica por email:

Screenshot from 2013-09-11 18:31:10Para ese resumen de conexiones que envía (SUMMARY REPORT LAST MINUTE), check_access usa otro pequeño programa en Python, summary_access, que nos permite obtener en cualquier momento resúmenes de cualquier tipo: por IP, por usuario, por script, IP del servidor web, etc. Por ejemplo, el siguiente es un resumen del usuario gallir (yo😉 ) durante las últimas dos horas:

Screenshot from 2013-09-11 18:41:08

El programa check_access, si se ejecuta como una comando en consola, tambien sirve para ver un resumen de las conexiones en tiempo real (nota: las IP que aparecen con la letra “B” son bloqueadas que siguen intentando, la 46.105.237.95 es de Deamule, así desde hace meses):

Screenshot from 2013-09-11 18:47:34

6. Cada script que se ejecuta en las instancias web, antes de responder nada verifican, si la IP del cliente está baneada (en la función check_ip_noaccess() de libs/utils.php) . Para minimizar el uso de recursos (sobre todo teniendo en cuenta que es para evitar DoS) se usa cache local en cada instancia (memcache o el sistema similar de xcache), por lo que el control tiene dos pasos:

  1. Al momento de la creación del objeto base de datos se ejecuta el primer paso, para ver si la IP está en la cache local. Si está ya no hace falta el siguiente paso, y se permite la continuación de la ejecución del script, o se rechaza la conexión (función reject_connection() en libs/utils) y se detiene la ejecución.
  2. Si la IP no fue encontrada en la cache, justo después de establecer la conexión para la primer consulta a la base de datos (método connect() en libs/rgdb.php) se hace la comprobación en la base de datos. Si la IP no está bloqueada, sólo se almacenará esa información en la cache local durante unos pocos segundos (actualmente, 4). Si la IP está bloqueada, se rechazará la conexión y se almacenará la IP en la cache durante un tiempo más largo (actualmente 20 segundos). De esta forma se evita consultas a la base de datos en las siguientes conexiones desde la misma IP y se podrá abortar la conexión más rápido, consumiendo muy pocos recursos.

Conclusiones

El problema en principio parecía complicado por la imposibilidad de usar mecanismos ya estándares de las IP tables por estar detrás de un balanceador de carga que actúa como proxy y sólo envía la IP de origen en las cabeceras HTTP (aunque ahora están desplegando IP proxy), además de no permitir ningún tipo de gestión de bloqueo (sí lo permiten vía los “grupos de seguridad”, pero si se rechazan IPs hay que enumerar todas las permitidas, es decir, todas las IPs posibles restantes… imposible). Al ser un sistema que evite DoS se debía minimizar también los recursos para detectar y responder, porque al fin y al cabo, las conexiones desde esas IPs se siguen haciendo, y se siguen ejecutando los scripts.

Pero esta solución funciona muy bien:

  • consume muy pocos recursos, es casi inapreciable para los lectores habituales (el trabajo adicional de logging se hace en la función de shutdown, cuando ya acabó todo y después de cerrar la conexión con el cliente -lo permite el php-fpm que usamos en Menéame),
  • el tráfico adicional que genera es relativamente muy bajo y fácilmente tratable por ambos extremos (el consumo de CPU del rsyslog es casi inapreciable), si hay un fallo del rsyslog los usuarios no lo notarán (están desacoplados),
  • fue muy fácil agregar unas pocas líneas al código de Menéame para generar los logs, y sólo re-usa lo que ya estaba instalado y funcionando antes (el rsyslog para los mensajes de error y aviso al “syslog”),
  • la programación de los scripts de Python para detectar los DoS y elaborar los resúmenes son muy sencillos y fácilmente adaptables a cualquier formato de log,
  • además obtenemos información adicional que nos permite analizar el comportamiento (frecuencia y tiempo de ejecución) de los diferentes scripts (lo que me ha llevado a re-programar el sistema de notificaciones, visto que el script notifications.json.php se ejecutaba en más del 40% de las conexiones, por ejemplo),
  • es KISS.

La única pega es quizás el síndrome Not Invented Here, pero es que no encontré un sistema ya programando y que fuese sencillo y eificiente para adaptar. Al final tardaría más tiempo estudiando y adaptando que programando algo simples desde cero. De hecho, en los foros de Amazon AWS se habla mucho de la necesidad de tener algo que haga lo que hacemos ahora, pero lástima que me da mucha pereza traducir este apunte al inglés para que se enteren cómo lo hicimos nosotros😉