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

Actualizar DNS con DHCP

En la entrada anterior vimos como montar un servidor bind y un server dhcp en linux para bloquear la resolución de algunas webs en nuestra red de área local. (Suplantando nuestros dns en la red local). Ya que tenemos el laboratorio montado en casa, podemos mejorar sus funcionalidades haciendo que todos los dispositivos que reciban una dirección DHCP de nuestro servidor queden registrados en el DNS con una entrada dinámica. Hacerlo a mano sería bastante laborioso y en breves tendríamos la zona dns desactualizada.

Para ello me he creado una zona a la que llamaré “canostra.local”, esta zona engloba mi red local. Siguiendo con el ejemplo anterior, tendremos 3 dispositivos en la red:

	192.168.22.1  --> Gateway
	192.168.22.2  --> Equipo Windows con reserva de DHCP
	192.168.22.11 --> DNS + DHCP.

Para poder crear el dynamic dns, el primer paso es editar el archivo para crear las nuevas zonas:

sudo vi /etc/bind/named.conf.local

Sigue leyendo

Suplantando nuestros DNS en la red local

Desde hace un tiempo, es muy común que la gente utilice su teléfono indiscriminadamente a todas horas. Esta mañana cuando desperté me dirigí a la cocina a saludar a mis padres y como es costumbre tras varios minutos apareció mi hermana escuchando un servicio de streaming de audio a todo volumen, le pedí por favor que lo desconectara y como es pasó de mi, así que esto basto para encender mi creatividad. Como no tengo el hardware necesario para montar un Firewall + Squid para que me aplique control parental, he decidido montar un aproximación con mi Raspberry (Ya os aviso de que es muy rudimentario, pero de momento efectivo). La finalidad de este post es poder bloquear el acceso a una serie de dominios en determinadas horas del día.

Evidentemente, para alguien que sepa algo de redes este procedimiento no sirve de nada, bastaría con forzar los dns de la máquina en la que se está trabajando a los de nuestro ips/google/opendns para que nos sirviera las entradas reales, pero creo que para mi propósito me sirve sobrado.

Antes de empezar supondremos que tenemos una RED de área local que corresponde con el rango 192.168.22.0/24 dónde

	192.168.22.1  --> Gateway
	192.168.22.2  --> Equipo Windows con reserva de DHCP
	192.168.22.11 --> DNS + DHCP.

En la siguiente imagen os presento una aproximación (en este caso no sería ROGUE DNS porque nosotros lo permitimos con nuestro servidor DHCP, ya que es nuestra red, pero si montáramos este escenario intentando suplantar el DHCP real se podría considerar un ataque) de lo que realizaremos:

Rogue DNS

En mi caso el Router del ISP no me permite configurar que servidores DNS serán servidos con este protocolo por lo que tendré que instalar un servidor de DHCP en la Raspberry y desactivar el propio del router.

Sigue leyendo

Let’s Encrypt la CA Open Source

Para todos aquellos que no conozcáis Let’s Encrypt os invito a que visitéis su Web . Esta entidad certificadora es Open Source y expide certificados gratuitamente siempre que podamos verificar la propiedad del sitio web mediante el protocolo ACME (automatic certificate management environment). Este protocolo nos permite administrar y expedir certificados de manera automática.

La única pega de esta entidad es que la validez de los certificados es de 90 días. Por lo que cada 3 meses hay que renovarlos en el servidor. Existen varios mecanismos de autenticación de un dominio, el más sencillo es la comprobación web donde la entidad certificadora nos solicita que creemos un .html con un contenido específico en nuestro dominio. Para automatizar el uso de ACME se creó un proyecto que se llama dehydrated en el que se gestionan en bash las llamadas a letsencrypt. Este proyecto es modular y permite parametrizar fácilmente las opciones de despliege de los certificados:

https://github.com/lukas2511/dehydrated

Este tipo de autenticación vía http presenta un problema, no puedes autenticar subdominios no publicados en Internet ya que la CA no podría conectarse a comprobar el challenge. Para este tipo de escenarios podemos utilizar la autenticación DNS01, donde se publica la información en un registro TXT del DNS que nos gestiona el dominio. Para usar este tipo de challenge, hay que especificar un “hook” a dehydrated para indicarle como tiene que interactuar con nuestro DNS.

En mi caso dispongo de una solución DNS de EfficientIP solid server, he creado un hook para usar dehydrated con SolidServer. Se puede encontrar en github:

https://github.com/berni69/solidserver-challenge

Para usarlo bastaría con descargarlo junto con dehydrated:

$ cd ~
$ git clone https://github.com/lukas2511/dehydrated
$ cd dehydrated
$ mkdir hooks
$ git clone https://github.com/berni69/solidserver-challenge.git hooks
$ chmod +x dehydrated
$ ./dehydrated --challenge dns-01  --cron --domain "test.example.com" --hook "hooks/solid-hook.py"

Saludos!