Shaping con nginx: Limitando CPS (II)

Hace unos días, se me planteó el problema de limitar las CPS (conexiones por segundo a un servicio de APIs externo), por lo que mi primera idea fue limitarlo en el proxy de salida de los servidores (SQUID). Pero eso implicaba cambiar y tunear las IPtables de una forma poco dinámica ya que si el proveedor está en una CDN habría que montar un proceso de actualización de reglas,.. o bien limitarlo para todas las salidas externas del proxy con lo que no resultaba ser una buena opción así que seguí buscando,… en este caso encontré nginx como proxy inverso con el módulo limit_req_zone que permite hacer rate limiting de peticiones.

Siguiendo el esquema de la entrada anterior (Shaping con Iptables: Limitando CPS):

Y la misma arquitectura de pruebas:

Nombre del Servidor IP Descripción
shaper01 10.112.112.101 Nginx Reverse Proxy
webserver01 10.112.112.102 Servidor web con nginx
server01 10.112.112.106 Envío de tráfico

Para simplificar configuraciones, y la lectura de los scripts se han hecho 2 suposiciones.

  1. Tenemos acceso privilegiado a la máquina (sudo -i) o similar.
  2. En el archivo de hosts (/etc/hosts) de cada máquina se ha añadido lo siguiente:
    10.112.112.101 shaper01
    10.112.112.102 webserver01
    10.112.112.106 server01
    

Para las pruebas se ha habilitado la siguiente zona en nuestro nginx (/etc/nginx/conf.d/rate-limit.conf):

limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;
server {
    listen       80;
    server_name  shaper01;
    location / {
       limit_req zone=mylimit;
       proxy_set_header Host "webserver01";
       proxy_pass http://webserver01;

    }
}

En esta zona se indica que todas las peticiones con el server_name “shaper01”, en la ruta / e hijas se aplicará un rate-limiting de 10r/s, y tras aplicar esta limitación serán reenviadas las peticiones contra el servidor webserver01.

El único cambio que hay que hacer en el programa cliente es la url dónde apuntarán sus peticiones, en este caso, se deberán abrir contra el servidor shaper01 en lugar del endpoint de la API.

Las pruebas se han lanzado con curl igual que en la entrada anterior:

while [ 1 ]; do curl shaper01 -s > /dev/null; done

Tras analizar los resultados del nginx del server web, podemos afirmar que está funcionando el rate-limiting.

Conexiones Por segundo usando nginx

Shaping con Iptables: Limitando CPS

Buenas noches, después de mucho tiempo sin pasarme por aquí, esta noche os traigo una pequeña entrada para ver como se puede configurar un rate limiting (shaping) en función de las conexiones por segundo en lugar del por ancho de banda (bandwidth).

Durante el transcurso de esta semana, me han consultado si se podría limitar el consumo de una API REST de un proveedor externo a priori no debería ser mucho problema, cuando te piden esto lo primero que piensas es en limitar el ancho de banda consumido en un packetshaper/firewall aplicando políticas de QoS. Pero esta vez, se solicitó que se limitara por transacciones por segundo permitidas. Tras revisarlo con los compañeros nunca habíamos montado nada así por lo que me he dispuesto a investigar un poco más y he llegado la solución que os contaré en este post.

En la siguiente imagen se describe la arquitectura inicial, en el recuadro rojo se muestra que existen N instancias de una aplicación que competirán por 10 conexiones por segundo contra la API externa (verde).


Sigue leyendo

Integración OSSIM – Elasticsearch

Qué es ossim?

El software de seguridad OSSIM, del inglés Open Source Security Information Management, es un conjunto de herramientas de seguridad (recogida de logs, monitorización, escaneo de vulnerabilidades,…) orquestadas para conseguir una solución de seguridad capaz de correlacionar y detectar eventos de seguridad y vulnerabilidades en una infraestructura determinada.

Tras haber gestionado ossim, me he dado cuenta que la integración de OSSIM con el mundo exterior es algo complicada.. expresiones regulares, creación de nuevos plugins, tener que pasar siempre por syslog,… Para evitar estos problemas se ha desarrollado un nuevo datasource de ossim para permitir la integración de OSSIM con elasticsearch.

El código se encuentra en mi repositorio público:

https://github.com/berni69/ossim-agent-elasticsearch

El código que se debe añadir a ossim es:

ParserElastic.py
ElasticDetector.py

Además se debe modificar el componente Agent.py (línea 559 aprox) para añadir un nuevo tipo de datasource.

                elif plugin.get("config", "source") == "elasticsearch":
                    parser = ParserElastic(self.conf, plugin, None)
                    parser.start()
                    self.detector_objs.append(parser)

Para que esto funcione se debe añadir la librería de elasticsearch para python:

pip install elasticsearch

Ejemplo de configuración:

https://raw.githubusercontent.com/berni69/ossim-plugins-elasticsearch/master/elasticsearch-example.cfg

Mantén tus dispositivos actualizados con Ansible

Si te gusta cacharrear como a mi, al final terminas teniendo varios dispositivos conectados a la red de tu casa y otros tantos distribuidos en el cloud, una de las tareas más tediosas a las que te puedes enfrentar es irlos actualizado a medida que pasa el tiempo. No a todos estos dispositivos me conecto habitualmente por lo que no siempre recuerdo actualizarlos, para solucionar esto he decidido que voy a usar Ansible. Esta pieza de software se define como:

Ansible es categorizado como una herramienta de orquestación. Maneja nodos a través de SSH y no requiere ningún software remoto adicional (excepto Python 2.4 o posterior para instalarlo). Wikipedia

Para poder instalar Ansible es necesario disponer de Pip y Python en nuestro sistema. Yo lo decidí instalar en mi Raspberry Pi por lo que además tuve que instalar software adicional:

apt-get update
apt-get upgrade python
apt-get install libssl-dev libffi5-dev python-dev build-essential
cd /tmp/
wget wget https://bootstrap.pypa.io/get-pip.py
python get-pip.py
pip install ansible

Dependiendo de la velocidad de la tarjeta SD de tu Raspberry Pi y la versión de ésta, puede tardar casi una hora en instalar todo el software necesario.

Una vez instalado el software, debemos crear el inventario de máquinas en /etc/ansible/hosts

mkdir /etc/ansible/
touch /etc/ansible/hosts

En mi caso voy a crear un grupo de servidores llamado servers que contiene un servidor vps y 2 raspberrys:

[servers]
mivps.example.com   ansible_connection=ssh  ansible_user=root
rpi1                ansible_connection=local #Host donde ejecuto ansible
rpi2                ansible_connection=ssh  ansible_user=pi    ansible_host=192.168.1.13

Donde la primera columna es el nombre del dispositivo al que nos vamos a conectar, ansible_host es la ip en el caso de que el nombre no se pueda resolver por dns y ansible_user es el usuario que usará ansible para conectarse al dispositivo.

Habitualmente, si no se especifica lo contrario, ansible intentará usar la clave ssh del usuario local para conectarse a los servidores remotos, si el usuario no tiene ninguna clave ssh generada, la crearemos.

mkdir $HOME/.ssh
chmod 600 $HOME/.ssh
ssh-keygen -t ecdsa -C "miusuario@miequipo"

Este proceso debe generar 2 claves (pública y privada) llamadas ~/.ssh/id_ecdsa.pub y ~/.ssh/id_ecdsa. Debemos copiar la clave pública (.pub) en el archivo authorized_keys del servidor remoto, para ello ejecutaremos el comando:

ssh-copy-id -i ~/.ssh/id_ecdsa root@mivps.example.com

Si queremos probar que se han copiado las claves ssh correctamente y tenemos acceso podemos ejecutar el comando

root@rpi1:~# ansible servers -m ping
mivps.example.com | SUCCESS => {
    "changed": false, 
    "ping": "pong"
}
rpi1 | SUCCESS => {
    "changed": false, 
    "ping": "pong"
}
rpi2 | SUCCESS => {
    "changed": false, 
    "ping": "pong"
}

Si todos los hosts aparecen como success, es que la instalación de claves ha ido correctamente.

Una vez copiadas todas las claves en los servidores, debemos crear el archivo /etc/ansible/update.yml:

---

- hosts: servers
  become: true
  become_user: root
  become_method: sudo
  tasks:
    - name: Update packages list [APT]
      apt: update_cache=yes
      when: ansible_os_family == 'Debian'
    - name: Upgrade packages [APT]
      apt: upgrade=safe
      when: ansible_os_family == 'Debian'
    - name: Upgrade packages [YUM]
      yum:
        name: '*'
        state: latest
        update_cache: yes
      when: ansible_os_family == 'RedHat'

Para finalizar, debemos lanzar el comando

root@rpi1:~# ansible-playbook /etc/ansible/update.yml

PLAY [servers] *********************************************************************************************************************************************************************************************

TASK [Gathering Facts] *************************************************************************************************************************************************************************************
ok: [mivps.example.com]
ok: [rpi2]
ok: [rpi1]

TASK [Update packages list] ********************************************************************************************************************************************************************************
changed: [mivps.example.com]
changed: [rpi2]
changed: [rpi1]

TASK [Upgrade packages] ************************************************************************************************************************************************************************************
changed: [mivps.example.com]
changed: [rpi1]
changed: [rpi2]

PLAY RECAP *************************************************************************************************************************************************************************************************
mivps.example.com          : ok=3    changed=2    unreachable=0    failed=0   
rpi1                       : ok=3    changed=2    unreachable=0    failed=0   
rpi2                       : ok=3    changed=2    unreachable=0    failed=0   

Si todos los hosts aparecen como 0 failed y 0 unreachable es que la ejecución del script ha ido correctamente.

Reiniciando automáticamente el router CG6640E

Desde hace un tiempo el router de ONO me falla cuando hace más de una semana que no se ha reiniciado, por lo que cada X tiempo me tenía que acordar de reiniciarlo para no quedarme sin servicio durante algún hito importante.

Trasteando un poco he visto que en su firmware existía una página que estaba oculta en los menús:

http://192.168.x.1/modem_configuration.html

Esta página, además de permitirte hacer un reset de fábrica del aparato, te permite cambiar las frecuencias favoritas del CM y hacer un reboot del dispositivo:

Configuración avanzada del modem de ONO

Con lo que con un poco de ayuda de las herramientas de desarrollador de google y unos cuantos reinicios del dispositivo he podido ver que estaba ejecutando por debajo:

http://192.168.x.1/setRestartCM.html

Eso es un avance, llamando a esa url se reinicia el módem siempre que haya un usuario logeado en el módem desde la IP que llama.

Por lo que he tenido que investigar como hacer login en el dispositivo, es un mecanismo muy sencillo que no usa ni cookies ni tokens ni sesiones, solo depende de la IP de origen de la petición:

http://192.168.x.1/login/Login.txt?password=xxxx&user=admin

Una vez hecho login la petición de login, el router devolverá el nombre del usuario si ha sido correcto o false si no ha funcionado el login.

Para finalizar, sólo es necesario hacer una petición http a cualquier html para que el reinicio se ejecute correctamente.

Si juntamos todos estos requisitos, queda el siguiente script en bash:


#!/bin/bash

USER="admin"
PASSWORD='password'
HOST="192.168.x.1"

#Login

echo "[+] Loging"

curl -v -G -XGET "http://${HOST}/login/Login.txt" --data-urlencode  "password=${PASSWORD}" --data-urlencode  "user=${USER}"


#Reboot
sleep 4
echo "[+] Rebooting"
curl -0 -v -XGET  "http://${HOST}/setRestartCM.html"

# Fake HTML

sleep 1
echo "[+] Sending Fake HTML"
curl -0 -v -XGET  "http://${HOST}/index.html"

Con un crontab para ahorrarnos el trabajo, quedaría un script capaz de reiniciar automáticamente el router con la frecuencia que le digamos.

Laravel: Securizando APIs

Desde hace un tiempo he estado trabajando en una apliación web para facilitar la gestión de servidores, para ello he desarrollado varias APIs para poder ejecutar comandos en servidores. Uno de los puntos más criticos de esto es permitir o denegar el acceso a los usuarios para llamar a estas APIs.

En mi caso, estoy usando el sitema de auth de Laravel con la librería Adldap2-Laravel para manejar accesos desde Active Directory. Por determinadas razones nececsito que mis usuarios puedan hacer llamadas via un token de autorización. Este paso es muy sencillo, Laravel con su módulo de Auth nos proporciona este mecanismo si utilizas el driver de base de datos y no el driver LDAP ya que no comprueba si el token que usas pertenece a un usuario dado de baja en ldap o le ha expirado la cuenta. Para corregir este comportamiento debemos hacer lo siguente.

Debemos añadir al archivo de configuración de autenticación (config/auth.php), los nuevos guards y providers “custom” que vamos a generar:

    'guards' => [
        ...
        'api' => [
            'driver' => 'token',
            'provider' => 'api-users',
        ],
    ],
    'providers' => [
        .....
         'api-users' => [
             'driver' => 'custom',
             'model'  => App\User::class,
         ],
    ],

El siguiente paso es indicar en el archivo de rutas de APIs que el guard de authenticación que queremos usar es el de ‘api’ (routes/api.php):

Route::middleware('auth:api')->get('/user', function (Request $request) {
    return $request->user();
});

Para que el provider de usuarios sepa que clase debe usar debemos crear nuestro proveedor e indicarselo a laravel:

app/Providers/CustomUserProvider.php

<?php
namespace App\Providers;

use Adldap\Laravel\Facades\Adldap;
use Illuminate\Contracts\Auth\Authenticatable as UserContract;
use Illuminate\Auth\EloquentUserProvider;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;

class CustomUserProvider extends EloquentUserProvider

{
	/**
	 * Retrieve a user by the given credentials. *
	 * @param array $credentials *
	 @return \Illuminate\Contracts\Auth\Authenticatable|null
	 */
	public

	function retrieveByCredentials(array $credentials)
	{
		if (empty($credentials)) {
			return;
		}
                // First we will add each credential element to the query as a where clause.
                // Then we can execute the query and, if we found a user, return it in a 
                // Eloquent User "model" that will be utilized by the Guard instances.
                $query = $this->createModel()->newQuery();
		foreach($credentials as $key => $value) {
			if (!Str::contains($key, 'password')) {
				$query->where($key, $value);
			}
		}

		$user = $query->first();
		if (!$user) {
			return;
		}

		// Securing API CALLS with LDAP checks

		$adldap_user = Adldap::search()->users()->find($user->id);
		if ($adldap_user->isExpired() || $adldap_user->isDisabled()) {
			return;
		};
		return $user;
	}
}

Dentro de la clase anterior hemos sobreescrito la función retrieveByCredentials para que además de comprobar el token del usuario compruebe si éste ha expirado en LDAP o está deshabilitado.

El siguiente archivo que debemos modificar es el AuthServiceProvider para que nos cargue nuestro proveedor ‘custom’:

app/Providers/AuthUserProvider.php


<?php 

namespace App\Providers;

use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider

	{
	/** * The policy mappings for the application. * * @var array */
	protected $policies = ['App\Model' => 'App\Policies\ModelPolicy', ];
	/**
	 * Register any authentication / authorization services.
	 *
	 * @return void
	 */
	public

	function boot()
		{
		$this->app['auth']->provider('custom',
		function ($app, array $config)
			{
			$model = $app['config']['auth.providers.users.model'];
			return new CustomUserProvider($app['hash'], $model);
			});
		$this->registerPolicies();

		//

		}
	}

Adicionalmente, como la librería de LDAP no está preparada para el uso de tokens, es necesarío parchear para que la primera vez que hace login el usuario le genere un token:

app/Controllers/Auth/LoginController.php

A este controller le debemos sobreescribir la funcion authenticated del siguiente modo:

    protected function authenticated(Request $request, $user)
    {
        if (empty($user->api_token)) {
            $user->api_token = str_random(60);
            $user->save();
        }
    }

De esta forma, la primera vez que se genera un usuario al no tener un token asignado lo generaremos al vuelo.

Para comrpobar si ha ido se está generando el token podemos revisar la base de datos o imprimirlo en el blade del siguiente modo:

  @if(Auth::check())
        <a href="api/user?api_token={{ Auth::user()->api_token }}">Mi usuario JSON</a>
  @endif