Información blog

Linux, tutoriales, noticias, sistemas, redes y seguridad informática, entre otras cosas.

martes, 17 de abril de 2018

Hotlinking; qué es y como evitarlo en Apache 2

Todos aquellos que tienen un servidor web expuesto al público, saben que al hacerlo dejan dicho servidor expuesto al mundo, y aún con un firewall bien configurado para evitar, en su medida, ciertos ataques, saben que siempre se corre riesgo de que por un motivo u otro, la web deje de estar disponible, su tiempo de respuesta sea lento o que ésta sea vulnerada. Teniendo en cuenta que el objetivo de una web es que "todos" la vean, es harto difícil lograr combinar una seguridad "infranqueable" y disponibilidad, y lo peor de todo es que a veces, acciones insignificantes que no son malintencionadas, pueden afectar al rendimiento de nuestra web... Hoy quiero hablaros de dicho último en especial; concretamente de una acción llamada hotlinking.

hotlink_portada

El hotlinking es una acción que si bien en sí no es maligna, puede perjudicar a otros en determinados aspectos, pues se trata de el uso de imágenes de otros servidores en vez de imágenes propias para enriquecer el contenido de una web. A modo de ejemplo, supongamos que queremos añadir una imagen a nuestra web; tenemos dos opciones, o subirla a nuestro servidor, o hacer referencia a la URL de una imagen ya hospedada en otro servidor; la segunda opción se trataría de un hotlink. Esto en sí no es un problema cuando se tratan de pocas imágenes y pocos links... Pero sí que puede ser problemático cuando el número de hotlinks es muy alto; especialmente si las páginas desde las que se reciben los hotlinks son muy visitadas, pues cada vez que dicha imagen es cargada en la otra página, estaría consumiendo ancho de banda de nuestro servidor, ya que la imagen que estarían solicitando estaría alojada en nuestro servidor, con lo que en verdad nos estarían haciendo una solicitud de imagen a nosotros y no a ellos.

Con el fin de que el ancho de banda de nuestra web no sea consumido involuntariamente, lo ideal es protegernos del hotlinking; lo cual afortunadamente es sencillo de implementar en Apache 2. Para ello hay dos tipos de prevención de hotlinking; aquel que directamente te pone muestra una imagen nula o aquel que te muestra una imagen concreta (y diferente) con el fin de dar a entender que están realizando una práctica no deseada contra nuestras imágenes.

Para poder aplicar dicha medida de protección en nuestro servidor, lo primero que necesitamos hacer es tener el módulo mod_rewrite activado en nuestro servidor web Apache2. Dicho módulo es instalado por defecto con Apache2, pero al mismo tiempo se encuentra desactivado por defecto, con lo que primero activaríamos dicho módulo, lo cual es tan sencillo como escribir el comando de a continuación como root:
a2enmod rewrite 
Veremos que nos solicita reiniciar Apache2 con el fin de hacer efectivo el cambio, pero todavía no lo haremos debido a que necesitaremos hacer un pequeño cambio en la configuración de este servicio para que pueda trabajar con el módulo rewrite... Esto es debido a que Apache2 por defecto está diseñado para no trabajar con dicho módulo y para no permitir que se "sobrescriban" las políticas del servidor web. En este caso, al querer modificar el comportamiento del servidor para prevenir el hotlinking, editaremos el fichero /etc/apache2/apache2.conf; teniendo en cuenta que lo más común es que la página web esté alojada en /var/www/ (o en uno de sus subdirectorios), cambiaremos lo siguiente:

Antes:
<Directory /var/www/>
        Options Indexes FollowSymLinks
        AllowOverride none
        Require all granted
</Directory>

Despues:
<Directory /var/www/>
        Options Indexes FollowSymLinks
        AllowOverride all
        Require all granted
</Directory>

Con dicho cambio realizado, ahora sí que procederíamos a reiniciar Apache2 mediante el comando:
service apache2 restart
Una vez tengamos los preparativo realizados, pasaremos a implantar nuestra protección contra hotlinks, lo cual lograremos gracias a la intervención de un fichero que crearemos llamado .htaccess, cuya función es rescribir la configuración usada para el servicio web dentro del directorio en el que se aloja ésta y, también, dentro de los subdirectorios de la susodicha. En mi caso en concreto la web se encontraría alojada en /var/www/html, web en la cual estaría hospedada la siguiente imagen:

no_hotlink_prevent

Dicha imagen se encontraría desnuda en estos momentos, es decir que podría ser accedida desde cualquier otra web, con lo que dentro del directorio /var/www/html crearíamos el fichero .htaccess con el siguiente contenido:

RewriteEngine on
RewriteOptions InheritDown
RewriteCond %{HTTP_REFERER} !^$
RewriteCond %{HTTP_REFERER} !^http(s)://(www\.)?192.168.1.8/.*$ [NC]
RewriteRule \.(jpg|jpeg|png|gif)$ - [NC]

Desgranemos cada línea con el fin de entender el contenido del fichero y no hacer un simple copia-pega.

La primera línea, RewriteEngine on; habilita la posibilidad de realizar las opciones "rewrite" que se ejecutan a continuación, acciones realizadas gracias al módulo rewrite de Apache2; en caso de carecer de dicha línea o tener dicho valor establecido a off.

La segunda línea, RewriteOptions InheritDown, la cual es opcional, sirve para que los subdirectorios hereden la configuración aplicada en dicho .htaccess.

La tercera y cuarta línea, son las condiciones bajo las cuales se ejecutaría el rewrite. En ambas se tiene en cuenta qué IP o dominio va a hacer referencia a la web debido a que en la condición, RewriteCond, se revisa dicho valor gracias a %{HTTP_REFERER}, después del cual se dice desde donde se realiza una petición HTTP al servidor. Dicha petición se realizaría desde dos orígenes, desde el propio servidor (a nivel interno) para lo cual pondríamos el parámetro en cuestión vacío, lo cual se representa como: !^$. En cambio, para especificar nuestro host, tendríamos que escribir el nombre (o la IP) de éste teniendo en cuenta que se puede hacer referencia al host como http, https (si lo tenemos habilitado), con www o sin este. En mi caso el servidor Apache2 sería la IP 192.168.1.8. Además, veremos que al lado hay un parámetro llamado [NC]; dicho parámetro se encargaría de no hacer distinción entre mayúsculas y minúsculas.

Por último se pondría la re-escritura de "reglas" mediante el RewriteRule, el cual en este caso haría que todas las peticiones que HTTP que se hagan a las imágenes jpg,jpeg,png y gif, sean bloqueadas. Por ejemplo, si hacemos una petición a dicha imagen desde otro servidor web, veremos una imagen en blanco:


hotlink_block
Esto nos serviría para bloquear el hotlinking desde otros sitios; si deseásemos que se mostrase una imagen concreta que fuese indicativa de que tenemos la protección contra hotlinks activada, podríamos editar el contenido anterior para que en la línea RewriteRule nos redirija a una imagen local previamente preparada. La re-dirección la haríamos especificando la ruta local de dicha imagen.

RewriteEngine on
RewriteOptions InheritDown
RewriteCond %{HTTP_REFERER} !^$
RewriteCond %{HTTP_REFERER} !^http://(www\.)?192.168.1.8/.*$ [NC]
RewriteRule \.(jpg|png)$ /var/www/html/imagenes/candado.png [NC]

Ahora en caso de intentar acceder a la imagen desde un origen no grato, veríamos la imagen de un candado tal y como podemos ver en la siguiente captura:

hotlink_redirect

Esta medida puede ser incluso más efectiva que la del bloqueo, ya que sería claramente disuasoria de que las personas intenten hacer hotlinks a dichas imágenes propias y que en caso de desear mostrarlas, tengan la necesidad de descargárselas y subírselas a sus propios servidores.

Como podéis ver la técnica de prevención de hotlinks es fácil y sencilla de aplicar, medida que si bien puede uno pensar que no necesita, puede prevenirle a uno del consumo indeseado de ancho de banda; prevención que se puede aplicar de una forma muy rápida como se ha podido ver aquí.

Espero que os haya resultado útil.

Saludos.

viernes, 23 de marzo de 2018

Cómo habilitar el tráfico PPTP a través de un router Linux

La creación de un router "casero" con Linux es relativamente sencillo; solamente necesitamos dos tarjetas de red y tener claras las IPs y VLANes a la que pertenece cada interfaz de red... La cuestión está en que a veces podemos tener problemas de comunicación con ciertos servicios al "aislarnos" gracias a dicho router casero que hemos creado con nuestro sistema Linux. Dicho aislamiento puede deberse a veces a problemas de configuración con iptables, o a que debido a algunas políticas por defecto adoptadas por Linux, cierto tráfico es bloqueado. En este caso quiero mostraros como habilitar el tráfico PPTP (Point to Point Tunelling Protocol) en Linux.

pptp_linux

Podemos tener una configuración perfecta en el cortafuegos, pero aún así ver que el tráfico no está llegando correctamente a su destino... Esto es debido a que Linux tiene deshabilitado por defecto el enrutamiento del tráfico PPTP, ya que hoy en día es considerado inseguro y es mucho más recomendado usar tecnologías tales como OpenVPN o tecnologías que usen Ipsec. Aún así, no siempre podemos trabajar en las circunstancias ideales y cuando nos piden conectarnos a un servidor VPN ajeno, no podemos elegir qué tipo de VPN es, sino que tenemos que adaptarnos a las circunstancias. Es por ello que tendremos que habilitar que dicho tráfico "fluya" por nuestro router.

Afortunadamente la solución es sencilla; lo primero de todo sería comprobar si tenemos el módulo el enrutamiento (también conocido como nateo) del protocolo PPTP activado. Esto es tan sencillo como listar todos los módulos existentes, haciendo un filtrado del módulo que buscamos, que en este caso sería nf_nat_pptp. Dicho listado lo haríamos gracias al comando lsmod:
lsmod |grep nf_nat_pptp
Lo más normal sería que el comando nos diese un resultado vacío; es decir que el módulo no estuviese activo. Afortunadamente la solución es extremadamente sencilla: Para insertar el módulo habría que ejecutar el comando:
insmod nf_nat_pptp
Gracias a dicho comando tendríamos activado el módulo en cuestión, si bien para que este cambio fuese permanente habría que incluirlo en el arranque añadiéndolo en al fichero /etc/modules. Para ello habría que escribir el comando:
echo 'nf_nat_pptp' >> /etc/modules
Ahora bien, con dicho comando no sería suficiente, habría que hacer que el kernel también fuese capaz de hacer las acciones necesarias para gestionar dicho tráfico. Afortunadamente el cambio solamente consistiría en añadir una línea al fichero /etc/sysctl.conf. Dicha línea sería añadida tal que así:
echo 'net.netfilter.nf_conntrack_helper=1' \ 
>> /etc/sysctl.conf
Para aplicara los cambios dentro de dicho fichero simplemente habría que ejecutar el comando:
sysctl -p
Gracias a este cambio, un sistema Linux que actúe como enrutador, enrutaría también el tráfico PPTP, brindándonos así la posibilidad de conectarnos a dichos tipos de VPN.

Espero que os haya sido útil.

Saludos.

miércoles, 21 de marzo de 2018

Límites en Linux; qué son y cómo controlarlos

A la hora de trabajar con servidores Linux, la prioridad de uno suele ser siempre la misma: Que sea capaz de dar servicio todo el tiempo posible y que sea capaz de atender cualquier petición que se le realice. Ambas necesidades tienen una estrecha relación con el hardware instalado, pero en muchas ocasiones se ignora el software, el cual también tiene un papel importante. Podemos tener un hardware extremadamente potente que si a nivel de software no tenemos la optimizaciones necesarias, no le estaremos sacando partido al susodicho debido a que tenemos unos límites demasiado estrictos; es por ello que hoy quiero hablaros sobre los límites en Linux.

limits_portada

Existen ciertas aplicaciones tales como las bases de datos y las aplicaciones web, que requieren abrir una ingente cantidad de ficheros: Ficheros temporales, bases de datos, ficheros de configuración, etc... Además, con las bases de datos podemos estar haciendo varias consultas o "aperturas" de fichero simultáneas que contarían como múltiples aperturas de fichero... Esto en servidores con una carga de trabajo relativamente baja no supone un problema, pero en servidores con altas cargas de trabajo sí que supondría un problema ya que podríamos superar el límite de ficheros abiertos de forma simultánea, dando como error interno el mensaje: "Too many open files". Esto es debido a que los límites impuestos por el sistema nos impiden abrir más ficheros, al igual que otros límites nos pueden impedir otras tareas.

Afortunadamente dichos límites pueden ampliarse de forma sencilla. Lo primero sería conocer nuestro límite actual, lo cual es tan sencillo como escribir el comando:
ulimit -n
En caso de tener los valores por defecto, el valor mostrado por la salida de dicho comando sería de 1024; es decir que el límite actual sería de 1024 ficheros abiertos. Esto puede parecer un valor muy alto, pero en un servidor dicho valor puede quedarse corto, especialmente si trabajamos con bases de datos como PostgreSQL que trabaja con una gran cantidad e ficheros temporales por cuestiones de rendimiento.

Para cambiar el límite actual de forma provisional, podemos recurrir al comando:
ulimit -n 99999
El problema de dicho comando sería que el límite solo valdría para el usuario y sesión actual; si cerramos sesión o reiniciamos el equipo, dicho límite se perdería y volvería a establecerse el de 1024. Para realizar un cambio más permanente tendríamos que editar el fichero /etc/security/limits.conf.

Dicho fichero establece los límites para diferentes usuarios y grupos. Por defecto su contenido está completamente comentado, con lo que cualquier límite que deseemos agregar tendrá que ser hecho a mano. Antes de añadir cualquier contenido, es importante entender su sintaxis, ya que será lo que nos ayudará a entender qué queremos añadir y cómo hacerlo. Cada límite que queramos añadir estará compuesto por cuatro secciones separadas por espacios entre sí; en concreto la estructura sería:

dominio tipo objeto valor


  • Dominio: Este valor es el que dictamina a qué usuario o grupo vamos a asignarle el límite. Con * se haría referencia a todos.
  • Tipo: Se puede elegir un límite duro (hard) o blando (soft). El blando sería el límite "por defecto" que se tendría en el sistema, mientras que el duro sería el límite que no se podría traspasar aún asignándoselo con el comando ulimit -n X. 
  • Objeto: El objeto especificaría a qué le deseamos poner el límite; se puede poner límite al número de "logueos" simultáneos con el mismo usuario con maxlogins, también se puede poner un límite para el número de procesos controlados por un usuario o grupo mediante nproc, o también puede establecerse el límite de ficheros abiertos mediante nofile. Existen otros objetos tales como data o fsize que establecerían los límites de tamaño de los datos y de los archivos respectivamente... Hay dos objetos cuyo límite es delicado y que es recomendable modificar con cuidado; dichos objetos serían nice; que indicaría el valor máximo de prioridad "nice" que se puede establecer a un proceso, y priority que establecería la prioridad por defecto asignada al programa ejecutado por el usuario o grupo. Ambos objetos pueden tener un valor negativo, pues la prioridad oscila entre -20 y 19 siendo -20 la prioridad más alta y 19 la más baja. Existen más objetos, pero los mencionados serían los más populares.
  • Valor: El valor sería simplemente el indicador del límite. Aquí se puede poner el valor que nosotros veamos necesario; se podría poner un valor bajo en algunos objetos tales como maxlogins, mientras que se podrían poner valores altos (pero definidos) para objetos tales como nofile. En caso de querer no ponerle limitador alguno se le pondría el valor -1, valor que haría que el límite fuese "infinito" ; la excepción sería para los objetos nice y priority, pues para ambos el valor -1 es un valor que se encuentra dentro del rango que manejan y carecen de ningún valor "infinito".

A sabiendas de cual es la estructura de un límite, vamos a plasmar un par de ejemplos:

Uno de los límites más "usados" sería el la cantidad de ficheros abiertos simultáneos, el cual, tal y como hemos visto antes se especificaría con el objeto nofile. Podemos crear un límite "duro" y uno "blando" para el usuario que, generalmente, más ficheros abrirá: El usuario root. Podemos poner un límite "blando" de 9999 ficheros y uno duro de 99999, lo cual se haría añadiendo lo siguiente:

root   soft   nofile  9999
root   hard   nofile  99999

Por otro lado, en caso de querer poner un límite de maximos logins simultáneos podemos hacer:

root   soft   maxlogins 4
root   hard   maxlogins 4

En caso de querer hacer referencia a un grupo en vez de a un usuario la sintaxis sería @nombre_grupo. Es decir que si deseásemos hacer que el número de procesos controlados por cualquier miembro del grupo usuarios, fuese infinito, haríamos tal que así:

@usuarios soft  nproc -1
@usuarios hard  nproc -1

Para que luego estos límites hagan efecto simplemente valdría con reiniciar el equipo o (en caso de que fuese con un usuario que no fuese root) cerrar sesión, pues las sesiones activas mantendrían los valores actuales.

Espero que os haya resultado útil.

Saludos.

lunes, 19 de febrero de 2018

Cómo registrar qué USBs se han conectado en Linux

La recopilación de información es cada día más importante; en servidores siempre lo ha sido, pero en los equipos de sobremesa ha ido cobrando importancia paulatinamente  y hoy en día se ha demostrado que es importante tener registrada toda la información por si en un futuro nos pudiese llegar a ser útil. Hoy quiero hablar sobre el registro de USBs en Linux. Linux tiene la gran ventaja de tener una enorme cantidad de logs, pero los logs no siempre te registran todos los datos que uno desea y corremos el peligro de que dichos logs sean borrados o se roten mediante utilidades tales como logrotate; utilidades especialmente comunes en servidores. Por eso puede ser interesante tener una herramienta corriendo en segundo plano cuya única función sea registrar qué USBs se han conectado/desconectado, la hora en la que lo han hecho, datos útiles tales como el número de serie y de paso, registrar su actividad a ser posible.

portada_registro_usb

En este caso no optaremos a herramientas de terceros sino que tiraremos de conceptos de Linux y los aglutinaremos en un script que automatice todo ello... Además, en este caso, no almacenaremos la información en un fichero en texto plano, sino que vamos a almacenarla en una mini base de datos en formato SQLite... El motivo por el cual se ha optado por dicha opción es simple: SQLite requiere instalar pocas herramientas en el equipo lo cual es perfecto en equipos de sobremesa; además es una base de datos que consume muy pocos recursos por parte del equipo y al mismo tiempo tiene la gran ventaja de seguir siendo una base de datos basada en el lenguaje SQL, con lo cual podríamos hacer consultar de su información mediante consultas SQL mediante SELECTs con sus respectivos filtros, etc... Haciendo que la consulta de la información en el futuro sea muchísimo más eficiente, con la contrapartida de que se requieren unos conocimientos básicos de SQL para poder sacarle partido.

Teniendo esto en cuenta, comencemos con la instalación de las dependencias necesarias para que nuestra solución funcione... Dependiendo de qué distribución tengamos y las herramientas que tengamos instaladas, puede ser que ya tengamos todo lo necesario, pero para asegurarnos haremos:
apt-get install sqlite
Teniendo todo preparado, pasaríamos a crearnos primero la mini base de datos que almacenará la información de los USBs. En mi caso en particular he creado la base de datos en /var y la he llamado listaUsbs.db con lo que su creación sería tal que así:
sqlite /var/listaUSBs.db
Esto no solo crearía la base de datos, sino que estaríamos dentro de ella, con lo que pasaríamos a crear la tabla en cuestión. En mi caso he llamado a la tabla listausbs y su creación (junto con sus respectivas columnas) sería la siguiente:
CREATE TABLE lista_usbs (NOMBRE,FECHA_DETECT,FECHA_DESCONEX,
IDVENDOR,IDPRODUCT,MANUFACT,IPRODUCT,SERIAL);
En esta base de datos guardaremos toda la información importante tal como el nombre completo, la fecha de detección del USB, el fabricante y el numero de serie... Información que puede sernos útil en el futuro. Con esto claro, podemos crear un script en el que por un lado monitoricemos en tiempo real toda la actividad USB mediante tcpdump, y por otro lado también podemos tener constancia de qué USB se ha conectado en qué momento; esto lo podemos lograr mediante el siguiente script, al cual yo he llamado registrador_usbs.sh y que ha sido probado en Debian 8:


#!/bin/bash

#CAPTURAMOS TODO EL TRAFICO USB
modprobe usbmon
nohup tcpdump -i usbmon1 -w /tmp/captura.cap &
while true;
do
        #COMPROBAMOS SI SE HA CONECTADO ALGUN USB NUEVO
        if [ ! -f /tmp/listausbs2.txt ];
        then
                touch /tmp/listausbs2.txt
        fi
        lsusb > /tmp/listausbs.txt
        diff /tmp/listausbs.txt /tmp/listausbs2.txt
        if [ $? -eq 1 ]
        then
                diff /tmp/listausbs.txt /tmp/listausbs2.txt > /tmp/cambio.txt
                cat /tmp/cambio.txt |tail -n +2 > /tmp/cambio2.txt
                while read CAMBIO;
                do
                echo ${CAMBIO} > /tmp/cambio3.txt
                cat /tmp/cambio3.txt |grep "<"
                if [ $? -eq 0 ];
                then
                        #SI ES UNA CONEXION NUEVA LA REGISTRAMOS
                        NOMBRE="$( cat /tmp/cambio3.txt |cut -d '<' -f 2 |cut -d ':' -f 2- | cut -d 'D' -f 2- )"
                        echo $NOMBRE
                        lsusb -D /dev/bus/usb/$(lsusb |grep "${NOMBRE}" |awk {'print $2'})/$(lsusb |grep "${NOMBRE}" |awk {'print $4'} |cut -d ":" -f 1) > /tmp/usb_detallado.txt
                        FECHA_DETECT=$(date)
                        FECHA_DESCONEX=''
                        IDVENDOR=$(cat /tmp/usb_detallado.txt |grep idVendor |awk '{for (i=2; i<NF; i++) printf $i " "; if (NF >= 2) print $NF; }')
                        IDPRODUCT=$(cat /tmp/usb_detallado.txt |grep idProduct |awk '{for (i=2; i<NF; i++) printf $i " "; if (NF >= 2) print $NF; }')
                        MANUFACT=$(cat /tmp/usb_detallado.txt |grep iManufacturer |awk '{for (i=2; i<NF; i++) printf $i " "; if (NF >= 2) print $NF; }')
                        IPRODUCT=$(cat /tmp/usb_detallado.txt |grep iProduct |awk '{for (i=2; i<NF; i++) printf $i " "; if (NF >= 2) print $NF; }')
                        SERIAL=$(cat /tmp/usb_detallado.txt |grep iSerial |awk '{for (i=2; i<NF; i++) printf $i " "; if (NF >= 2) print $NF; }')
                        sqlite /var/listaUSBs.db "INSERT INTO lista_usbs VALUES ('${NOMBRE}','${FECHA_DETECT}','${FECHA_DESCONEX}','${IDVENDOR}','${IDPRODUCT}','${MANUFACT}','${IPRODUCT}','${SERIAL}');"
                        rm /tmp/usb_detallado.txt
                fi
                cat /tmp/cambio3.txt |grep ">"
                if [ $? -eq 0 ];
                then
                        #SI ES UNA DESCONEXION ACTUALIZAMOS LOS DATOS CORRESPONDIENTES
                        NOMBRE="$( cat /tmp/cambio3.txt |cut -d '>' -f 2 |cut -d ':' -f 2- | cut -d 'D' -f 2- )"
                        FECHA_DESCONEX=$(date)
                        sqlite /var/listaUSBs.db "UPDATE lista_usbs SET FECHA_DESCONEX='${FECHA_DESCONEX}' WHERE FECHA_DESCONEX='' AND NOMBRE='${NOMBRE}';"
                fi
                done < /tmp/cambio2.txt
                rm /tmp/cambio.txt
                rm /tmp/cambio2.txt
                rm /tmp/cambio3.txt
        fi
        mv /tmp/listausbs.txt /tmp/listausbs2.txt
done

Con este script, solamente ejecutándolo en segundo plano estaríamos monitorizando en tiempo real toda la actividad mediante tcpdump, y registrándola en nuestra base de datos SQLite. Solamente habría que escribir:
./registrador_usbs.sh >/dev/null &
Dicha información podría ser consultada en el futuro mediante consultas SQL tales como:

Para ver todos los registros:
sqlite /var/listaUSBs.db "SELECT * FROM lista_usbs;"
Para ver un numero de serie concreto:
sqlite /var/listaUSBs.db "SELECT * FROM lista_usbs WHERE 
SERIAL='3 20060413092100000';"
Para eliminar todos los registros:
sqlite /var/listaUSBs.db "DELETE FROM lista_usbs;"

Con ello tendríamos todo monitorizado y registrado en nuestro equipo ganando un gran control sobre la actividad USB en nuestro equipo. Obviamente dicho script sería solamente un ejemplo teórico que puede ser modificado a nuestro gusto para que se adapte lo mejor posible a nuestras necesidades.

Espero que os haya resultado útil.

Saludos.

jueves, 8 de febrero de 2018

Cómo crear un RAID por software mediante mdadm en Linux

Cuando hablamos de servidores y/o equipos que tienen que almacenar un gran volumen de datos importantes, nos vemos obligados a usar tecnologías que aseguren la integridad de nuestros datos en caso de desastre, mediante la tecnología RAID, pues es gracias a ésta que la rotura de un disco duro no es tan "grave" como debería. Generalmente la configuración de discos en RAID se suele hacer desde la propia controladora que incluye el hardware, pero no siempre podemos trabajar en las circunstancias ideales, y ahí es donde debemos de optar por una solución algo menos cómoda, pero igual de útil. La creación de RAID por software, que en este caso realizaremos gracias a la herramienta mdadm.

RAID_portada

Antes de proseguir, creo que es interesante que todo aquel que no esté familiarizado con la tecnología RAID, y con los diferentes tipos, tenga una pequeña noción tanto del RAID como de los distintos tipos de éste. 

RAID (Redundant Array of Independent Disks) permite usar un espacio de almacenamiento repartido entre varios discos para aumentar el rendimiento y/o la tolerancia a fallos. Dependiendo del tipo de RAID escogido, cuya elección haríamos dependiendo de nuestras necesidades, el RAID nos otorgaría unas ventajas u otras, con lo que es interesante conocer los tipos RAID más comunes con el fin de saber qué elección es la más adecuada para nosotros.


RAID 0:

De entre todos los RAID, probablemente sea el más "inútil", pues su única ventaja es que reparte toda la información en todos los discos que componen el RAID... Esto tiene como ventaja que si tenemos dos discos duros de 1 TB en RAID 0, tendríamos 2TB disponibles para almacenar lo que queramos, si bien tiene una gran desventaja y es que al repartir todo el contenido entre los dos discos, en caso de que uno de los dos fallase, toda la información se perdería, con lo que podríamos decir que el objetivo de este RAID se basa en la rapidez y la eficiencia, dejando de lado cualquier medida que pueda ayudarnos a no perder la información (más allá de las típicas, y al mismo tiempo infravaloradas, copias de seguridad).

RAID_0

RAID 1:

Este sería justo lo contrario al RAID 0, pues así como antes la única función del RAID 0 es la rapidez de acceso a los datos y la capacidad de usar varios discos como si fueran uno solo, en este caso el objetivo del RAID 1 es la tolerancia a fallos. En este caso, RAID 1 lo que haría sería clonar lo guardado en un disco en otro, con el fin de que en caso de que el "primer" disco falle, tengamos el segundo con la información íntegra disponible.

RAID_1

RAID 5:

Probablemente el más famoso de todos en el ámbito de servidores. En este caso no podemos realizar un RAID 5 con solo 2 discos, sino que harían falta un mínimo de 3. En este caso, el RAID 5 combina capacidad y tolerancia a errores, pues se trata de un RAID que usa tres cuartas partes de cada disco para el espacio en sí, mientras que una cuarta parte de cada uno de ellos es dedicada para la tolerancia a fallos. Además, en caso de que uno de los discos se averiase, tan pronto como se sustituyese, el nuevo disco se llenaría automáticamente con la información que le corresponde gracias a que los otros dos discos saben qué información le falta. Estos gracias a que distribuye cada ciertos bloques de información un bit de paridad permite saber qué información sería necesaria "rellenar" en caso de fallar un disco duro. En caso de que un segundo disco fallase (sin haber llegado a sustituir el primero), se perdería toda la información.

RAID_5



Teniendo estos conceptos claros, podemos pasar a la creación y/o configuración de RAIDs; para ello, lo primero e indispensable sería tener instalada la herramienta mdadm. Dicha herramienta, si bien viene incluida por defecto en los repositorios, no está instalada en el sistema, con lo que si queremos instalarla deberíamos hacer:
apt-get install mdadm
Una vez tengamos la herramienta instalada; la creación de volúmenes RAID es bastante sencilla; el único requisito es que tengamos dos discos duros (que pueden ser discos duros virtuales) o más para RAID 0 o RAID 1 y tres discos o más para RAID 5. Teniendo esto en cuenta, la sintaxis para la creación de un nuevo RAID por software sería:

mdadm -C nombre_raid -l tipo_raid -n número_discos discos_duros_a_usar nombre_discos

Si esto se desgranase significaría lo siguiente:

  • -C implica que se va a crear un nuevo RAID, al cual se le va a poner un nombre identificativo que el sistema pueda reconocer como si fuese un dispositivo más. Por norma general se usa como nombre /dev/mdx; por ejemplo /dev/md0.
  • Con el parámetro -l le estaríamos diciendo qué tipo de RAID queremos... Aquí solamente habría que indicarselo de forma numérica. Es decir que si quisiésemos un RAID 0 solamente especificaríamos el número 0, RAID 1 con el número 1, etc...
  • Gracias al parámetro -n estaríamos indicando el número de discos que van a componer el RAID, tras lo cual indicaríamos el nombre de las unidades que van a componer el susodicho.

Supongamos que vamos a crear un RAID 0 y que tenemos dos discos listos para ser usados llamados /dev/sdc y /dev/sdd. Para componer dicho RAID escribiríamos:
mdadm -C /dev/md0 -l 0 -n 2 \
/dev/sdc /dev/sdd
Dependiendo del espacio de cada disco, el RAID tardaría desde segundos hasta varios minutos. Tras crear dicho RAID podemos activarlo o desactivarlo a nuestro gusto mediante los siguientes comandos:

Para pararlo:
mdadm -S /dev/md0
Para arrancarlo:
mdadm -A /dev/md0 /dev/sdc /dev/sdd
Gracias a ello podemos tener activo el RAID solo cuando nosotros lo veamos conveniente, si bien obviamente el tener inactivo el RAID haría que no pudiésemos acceder a los discos.

En caso de tener el RAID activo, podríamos consultar el estado de este gracias al comando:
mdadm -D /dev/md0
En mi caso en particular, la información mostrada sería la de a continuación:

mdadm_D

Es importante recalcar que aunque el RAID esté creado, este carece de partición ni formato alguno... Con lo que con el fin de que sea usable habría que usar la utilidad fdisk, y crear una nueva partición:
fdisk /dev/md0
En mi caso habría creado una sola partición cuyo nombre sería md0p1. A dicha partición habría que darle formato, ya que por defecto no posee ninguno y no puede ser montado en GNU/Linux sin él. Así que le asignaríamos un nuevo formato mediante la herramienta mkfs; por ejemplo ext4:
mkfs.ext4 /dev/md0p1
Gracias a dicho comando, el RAID tendría una partición plenamente funcional que podría ser montada en el sistema como una partición más mediante el comando mount. Por ejemplo:
mount /dev/md0p1 /mnt
Obviamente dicha partición podría ser montada durante el arranque si se incluyese en /etc/fstab.

Como podéis ver la creación y gestión de RAIDs, y el convertirlos en volúmenes funcionales es más sencillo de lo que parece; simplemente habría que tener conocimiento de cada tipo de RAID y la ventaja que ofrece cada tipo.

Espero que os haya resultado útil.

Saludos.

jueves, 1 de febrero de 2018

Cómo monitorizar eventos de un directorio en tiempo real con inotify en Linux

En ocasiones podemos vernos en la situación de querer tener un control de lo que ocurre dentro de un directorio; tal es así el control que queremos que podemos llegar a desear ver todos los eventos en tiempo real que ocurren dentro de un directorio, eventos tales como el listado de archivos, la creación y modificación de estos e incluso la eliminación. Generalmente podemos intentar jugar el comando history para ver los últimos comandos, examinar las últimas fechas de modificación de los ficheros, e incluso examinar sus metadatos, con el fin de averiguar lo máximo posible de lo que ocurre en nuestro equipo; especialmente cuando éste está siendo accedido por varios usuarios a la vez o simplemente tenemos miedo de que lo esté siendo. Es por ello que para ocasiones como esas puede ser muy interesante recurrir a herramientas de observación de eventos en tiempo real para Linux tales como inotify.

Inotify_portada

Inotify es una herramienta con la capacidad de observar eventos en tiempo real dentro de un directorio, lo que nos otorga un gran conocimiento en comparación con otras herramientas, si bien, tal y como he matizado al principio, requiere monitorizar en tiempo real para poder ser funcional. Esta utilidad no está instalada por defecto en el sistema, sino que requiere ser instalada, si bien afortunadamente es realmente sencillo pues está incluida dentro de los repositorios oficiales del sistema:
apt-get install inotify-tools
Esto hará que se instalen dos herramientas: Inotifywait e Inotifywatch. El que nos interesa en concreto es Inotifywait pues ofrece una enorme información.

Un modo muy básico de usar Inotifywait sería de la siguiente forma:
inotifywait /tmp
El problema que tiene usar este comando directamente es que efectivamente escuchará los eventos de dicho directorio, pero no será capaz de escuchar los eventos de los subdirectorios de éste y además al recibir un solo evento inotifywait se cerraría automáticamente. Es por ello que conviene conocer cómo usarla para poder sacarle de verdad provecho.

Hay diferentes parámetros y opciones, pero los más importantes serían los de a continuación:

Comencemos con el parámetro -m; La inclusión de este parámetro hará que inotify entre en modo monitorización, con lo que podrá recibir eventos continuamente sin llegar a cerrarse a menos que nosotros le mandemos la señal necesaria para que se detenga. Dicho parámetro también podría especificarse también mediante --monitor. Otro parámetro muy útil sería el -r; Al usar este parámetro estaríamos indicando que se monitorizase todo el directorio en cuestión y también todos los subdirectorios. El parámetro en cuestión también podría especificarse mediante --recursive. El último que en mi opinión es también importante tener en cuenta sería el -e; Este último es más "opcional" que los dos anteriores, pero puede ser de una enorme utilidad, ya que sirve de filtrador de eventos; es decir que con dicho parámetro veríamos los eventos que nosotros queramos y no todos, que sería lo que se nos mostraría por defecto. El parámetro también puede ser llamado mediante --events. Obviamente, para poder filtrar los eventos, habría que conocer cuales son... Son un número considerable y, desgraciadamente, no todos son fáciles de recordar, pero es bueno conocer al menos su existencia:

  • access: Este sería uno de los más veríamos, ya que indica que un fichero o el contenido de un directorio ha sido leído. 
  • modify: Este parámetro haría que se nos notifique de los cambios que se realicen en el entorno que estamos monitorizando.
  • attrib: Este es ligeramente más raro que el resto, pues solamente notificaría los cambios en los atributos de los ficheros o directorios.
  • close_nowrite: Se notifica cuando se ha cerrado un fichero o directorio que ha sido abierto en modo solo lectura.
  • close_write: Se notifica cuando se cierra un fichero o directorio en modo escritura.
  • open: Especifica que un fichero o directorio ha sido abierto; este mensaje también es de los que más aparecen junto con access.
  • moved_to: Este evento implica que algo ha sido movido hacia el directorio que estamos monitorizando.
  • moved_from: Este evento indica que algo ha sido movido desde el directorio que estamos monitorizando.
  • move: Este evento indica que va a haber una actividad de movimiento dentro del directorio que monitorizamos, ya sea desde o hacia el directorio en cuestión.
  • create: En este caso se indicaría que algo (ya sea fichero o directorio) ha sido creado dentro del lugar que estamos monitorizando.
  • delete: Al contrario que create, en este caso se detectaría que algo se ha eliminado.
  • delete_self: Esto indica que el fichero o directorio que estamos monitorizando acaba de ser eliminado.
  • umount: Aquí se detectaría que el sistema de ficheros en que se aloja aquello que estamos monitorizando, acaba de ser desmontado.

Es conveniente resaltar que generalmente querremos ver todos los eventos, pero es importante conocer qué eventos se monitorizan para así poder hacer filtrados en caso de tener una necesidad muy concreta...

Teniendo estos conceptos claros podemos hacer cosas como la siguiente:
inotifywait -r -m /tmp
Con dicho comando estaríamos monitorizando todos los eventos dentro de la carpeta /tmp y todos sus subdirectorios; además estaría recibiendo constantemente eventos hasta que se le indique lo contrario, con lo que si abriésemos otra terminal podríamos realizar las acciones de a continuación:
cd /tmp && ls && touch test && echo prueba > test && rm test
Si viésemos los eventos visualizados por inotifywait veríamos lo siguiente:

inotifywait

Al final se estarían viendo en tiempo real todas las acciones realizadas, desde la creación del archivo hasta la eliminación de éste. Además si queremos podemos dejar que el proceso inotifywait esté corriendo en segundo plano y que vuelque todo el contenido en fichero para poder analizarlo más tarde con tranquilidad, extrayendo las conclusiones que deseemos. Esto requeriría hacer simplemente la siguiente modificación en el anterior comando:
inotifywait -r -m /tmp > /var/inotify.log &
Dicho proceso tendría que ser finalizado mediante un kill, o llevado a primer plano para poder finalizarse.

Como podéis ver el tener controlado un directorio o un grupo de éstos puede llegar a ser útil pues gracias a ello podemos tener un mayor conocimiento de lo que ocurre dentro de nuestro sistema.

Espero que os haya resultado útil.

Saludos.

lunes, 15 de enero de 2018

Cómo usar los playbooks con Ansible en Linux

Continuando con el anterior artículo, en esta ocasión vamos a profundizar un poco más en el uso de la herramienta Ansible. Hasta ahora hemos podido mandar ordenes individuales desde el controlador al resto de equipos, a los que llamaremos nodos, pero el problema radica en que por el momento lo único que estamos haciendo es mandar una orden individual a todos... Una orden que puede ser muy práctica y que puede ahorrarnos mucho tiempo, pero que no es lo más práctico cuando queremos gestionar varios servicios y tareas... Es por ello que en este aparado vamos a centrarnos en uno de los aspectos más importantes que ofrece Ansible: Los playbooks. Antes de continuar, es recomendable que, si no se tienen noción alguna de Ansible, os leáis el anterior artículo.

playbooks_portada

Un playbook no es más que un fichero que contiene las ordenes correspondientes que queremos enviar a todos los nodos; ordenes que se irían ejecutando secuencialmente según hallamos escrito en el fichero. Dicho playbook, estará escrito en lenguaje YMAL y tendrá una extensión .yml. La lógica es parecida a la seguida con las ordenes y los módulos usados en el anterior apartado, pero con más opciones y con la posibilidad de crear un conjunto de ordenes de forma estructurada y ordenada. La mejor forma de entenderlo es con un pequeño ejemplo inicial. Para ello, para tener todo bien organizado, vamos a crear un directorio llamado PLAYBOOKS dentro de /etc/ansible y dentro de dicho directorio vamos a crear un fichero llamado master.yml.
mkdir /etc/ansible/PLAYBOOKS
touch /etc/ansible/PLAYBOOKS/master.yml
Dicho fichero tendrá el contenido de a continuación:

---
#Este es el playbook maestro
- hosts: all
  remote_user: root
  tasks:
  - name: Asegurarse que NTP esta en marcha
    service: name=ntp state=started enabled=yes
...

Puede parecer complicado, pero si lo desgranamos, veremos que es muy sencillo:

  • Los primeros tres guiones indican el comienzo del fichero. Todo fichero YMAL debe llevarlo para que Ansible pueda leerlo correctamente.
  • Cualquier línea que comience con una # será considerado un comentario, tal y como ocurre en los scripts de bash.
  • Después comenzaremos con la primera sección de todas, que sería de a qué grupo (especificado previamente dentro del fichero /etc/ansible/hosts) queremos hacer referencia. Allí podemos poner el nombre de un grupo que hayamos creado o, en caso de quererlo, decir que se envíen a todas las IPs y hosts que aparezcan dentro del fichero. En nuestro caso hemos optado por dicha opción, lo cual se representa mediante - hosts: all. Es muy importante comenzar con dicho - pues marcaría el inicio de las acciones y reglas dirigidas a dicho grupo.
  • Tras decir eso, habría que decir a qué usuario remoto nos queremos conectar. Aquí podemos escoger el que queramos, pero en mi caso, ya que he preparado el usuario root del otro equipo para poder conectarme directamente a él sin contraseña alguna, optaré por usar dicho usuario.
  • Ahora habría que pasar a la parte de las tareas... Dichas tareas se definen mediante, el nombre tasks: y después debajo de éste empezaríamos a mencionar las tareas una por una; tareas que siempre empezarían con un -.
  • Toda tarea está compuesta por, cómo mínimo, 2 partes. La primera parte sería meramente informativa. Se pondría un nombre descriptivo que nos mostraría Ansible antes de ejecutar la tarea. Dicho nombre se especificaría mediante - name. La segunda parte sería la tarea a ejecutar, que constaría de: Módulo: argumentos. En este caso hemos usado el módulo service, y los argumentos usados serían name, state y enabled. Se pueden poner tantas tareas como se quieran, siempre y cuando se siga la misma estructura que la que acabo de mencionar.
  • Por último, siempre al final de cada playbook habría que escribir tres puntos.

Este ejemplo es muy sencillo y puede evolucionar a mucho más, poniendo diferentes hosts y tareas, pero sirve de buena base introductoria... Para ejecutar dicho playbook, simplemente habría que escribir el comando:
ansible-playbook /etc/ansible/PLAYBOOKS/master.yml
El contenido del playbook, al hacer referencia a una única tarea, sería lo equivalente al siguiente comando de Ansible:
ansible all -m service -a \
"name=ntp state=started enabled=yes"
Vamos a poner un ejemplo más completo en esta ocasión; supongamos que queramos asegurarnos de que el servicio NTP tenga la última versión; además también vamos a verificar que el servicio Asterisk está en marcha. Por otro lado vamos a crear una tarea exclusiva para el grupo llamado PRUEBAS; una tarea que consistirá en revisar que tenemos la última versión del paquete apache2. Al final sería aplicara los mismos conceptos que hemos visto arriba, pero de forma un poco más amplia, dejando el fichero con el siguiente aspecto:

---
#Este es el playbook maestro

#Reglas para todos los hosts
- hosts: all
  remote_user: root
  tasks:
  - name: Asegurarse que NTP esta en marcha
    service: name=ntp state=started enabled=yes
  - name: Verificar ultima version NTP
    apt: name=ntp state=latest
  - name: Asegurarse que Asterisk esta en marcha
    service: name=asterisk state=started enabled=yes

# Reglas para el grupo PRUEBAS
- hosts: PRUEBAS
  remote_user: root
  tasks:
  - name: Verificar ultima version Apache2
    service: name=apache2 state=started enabled=yes
...

Como podéis ver siempre mantendrá la misma lógica... Es decir primero irían los hosts de destino, luego el usuario con el que queremos hacer las acciones y por último las tareas a realizar.

Una característica muy interesante que podemos aprovechar en los playbooks, es el uso de handlers. Un handler es una tarea que se ejecuta únicamente en caso de que una tarea concreta (especificada por nosotros) haya realizado un cambio de estado. Por ejemplo si tuviésemos un handler preparado para cuando hubiese un cambio de estado en Apache2, y una tarea que se asegurase de que apache2 tuviese la última versión, en caso de que Apache2 no la tuviese, el handler se ejecutaría. Veamos un ejemplo usando como referencia el mencionado Apache2:

---
#Este es el playbook maestro
- hosts: all
  remote_user: root
  tasks:
  - name: Verificar ultima version Apache2
    apt: name=apache2 state=latest
    notify: "Reiniciar Apache2"
  handlers:
  - name: Reiniciar apache si es necesario
    service: name=apache2 state=restarted
    listen: "Reiniciar Apache2"
...

Como podéis observar, el handler está a la escucha de que le llegue una notificación; notificación que enviaríamos desde la tarea de verificación de la última versión de Apache2. Con lo que siempre que trabajásemos con handlers, trabajaríamos de la misma forma que con las tareas, pero con la diferencia de que desde la tarea tendríamos que notificar al handler mediante un notify, y desde el handler tendríamos que escuchar a que nos llegase una notificación concreta mediante un listen.

Gracias a las tareas (tasks) y handlers, podemos realizar playbooks muy completos donde podemos agrupar una enorme cantidad de tareas y handlers para luego así llamarlos a todos a la vez; ahorrándonos tener que escribir los comandos uno a uno para cada tarea que queramos realizar, haciendo que nos ahorremos muchísimo tiempo una vez tengamos todas las tareas correctamente definidas. Aún así, a nivel de gestión, el tener todas las tareas y handlers en un solo playbook puede ser una locura a nivel de mantenimiento... Cuando son pocas tareas y servicios no pasa nada, pero y si queremos gestionar 50 servicios, con diferentes grupos, handlers, etc... Técnicamente al agruparlo todo en un solo playbook es perfectamente viable, pero a nivel mantenerlo en el tiempo puede ser realmente costoso, especialmente si se quieren hacer cambios en el futuro... Es por ello que lo ideal es tener un playbook maestro y que este vaya llamando a diferentes playbooks dependiendo de las tareas que se quieran realizar. La llamada a dichos playbooks desde el playbook maestro se realiza mediante la sentencia include, y podemos crear una estructura perfectamente definida para que con el paso del tiempo se pueda mantener sin demasiados problemas.

Vamos a suponer que queremos poner un firewall para todos los equipos; un firewall común que todos van a tener; además también vamos a verificar que tanto Mysql como Apache2 están en marcha, pues tenemos un servidor web; por otro lado también verificaremos qué Asterisk tiene la última versión y que en dicho caso lo reiniciaremos. Estas tareas se podrían considerar como diferentes entre sí y por ello lo suyo sería tener: El playbook maestro, un playbook para el firewall, otro playbook para el apartado web y otro más para Asterisk. Comencemos con el aspecto del playbook maestro, master.yml:

---
#Este es el playbook maestro
- hosts: all
  remote_user: root
  tasks:
  - include: /etc/ansible/PLAYBOOKS/Firewall.yml
  - include: /etc/ansible/PLAYBOOKS/Web.yml
  - include: /etc/ansible/PLAYBOOKS/Asterisk.yml
  handlers:
  - include: /etc/ansible/HANDLERS/Firewall_handler.yml
  - include: /etc/ansible/HANDLERS/Web_handler.yml
  - include: /etc/ansible/HANDLERS/Asterisk_handler.yml
...

Como podéis ver todo queda alojado en ficheros separados, tanto las tareas como los handlers; handlers que como podéis ver se alojarían en otro directorio llamado /etc/ansible/HANDLERS/. Estos ficheros tendrían una ligera variación con respecto a lo que hemos visto hasta ahora, pues a diferencia de un playbook "normal", en estos no haría falta ponerles las etiquetas, solamente las tareas a realizar, pues ya estarían asignadas previamente por el master.yml. Veamos por ejemplo el contenido de uno de los playbooks de tareas; concretamente el contenido del fichero Firewall.yml que posee un módulo que no hemos visto hasta ahora; el módulo copy en el que copiaremos un fichero desde nuestro controlador al resto de nodos.

---
#ESTE ES EL PLAYBOOK DEL FIREWALL
- name: COPIAR IPTABLES DEL CONTROLADOR A LOS NODOS
  copy: src=/usr/src/iptables.sh dest=/etc/init.d/iptables.sh
  notify: "CORTAFUEGOS"
...

Como veis directamente empezaríamos con el - name; sin mencionar los hosts, ni el usuario remoto, ni el hecho de que es una tarea y no un handler, pues todo eso ya ha sido especificado por el anterior playbook. En este caso lo que hacemos simplemente es copiar un fichero desde la máquina controladora al resto de equipos y que en caso de que haya una diferencia entre el iptables.sh del equipo controlador y el del nodo, mande un notify llamado "CORTAFUEGOS". Obviamente si el fichero iptables.sh no existiese en el nodo, también mandaría dicho notify.

El proceso de llamamiento de un handler mediante un include no difiere apenas con el realizado con las tareas. Buen ejemplo de ello sería el handler Firewall_handler.yml que estaría relacionado con la tarea arriba mostrada; en este caso usaremos otro módulo nuevo llamado file, capaz de modificar los permisos de un fichero para que sea ejecutable:

---
#HANDLERS FIREWALL
- name: Dar permisos firewall
  file: dest=/etc/init.d/iptables.sh mode=755 state=touch
  listen: "CORTAFUEGOS"
- name: Arrancar cortafuegos
  shell: /etc/init.d/iptables.sh start
  listen: "CORTAFUEGOS"
...

En este caso el handler lo primero que haría sería darle los permisos de ejecución necesarios al nuevo script para luego ejecutarlo. El contenido del cortafuegos no es algo relevante para este ejemplo, pero a modo de prueba podría ser algo como lo siguiente:


#!/bin/bash
case "$1" in
        start)
                iptables -F
                iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
                iptables -A INPUT -m state --state NEW -j DROP
                iptables -A OUTPUT -j ACCEPT
        ;;
        stop)
                iptables -F
        ;;
esac

Con esto ya tendríamos el firewall controlado y simplemente siguiendo las mismas pautas se podría hacer exactamente lo mismo para la gestión de tanto la parte Web como Asterisk, pues la base sería la misma que la que hemos estado usando hasta ahora.

Gracias a lo que hemos visto en este artículo, tendríamos un dominio básico de los playbooks gracias al cual podríamos realizar la mayoría de las gestiones necesarias para controlar un grupo de servidores... De aquí en adelante solo sería profundizar conceptos y jugar con los diferentes módulos de Ansible, con el fin de tener los playbooks más óptimos posibles.

Espero que os haya resultado útil.

Saludos.

lunes, 8 de enero de 2018

Cómo instalar Ansible y usarlo de forma básica en Linux

La gestión de múltiples equipos es una tarea que dependiendo del volumen de éstos, puede resultar una tarea abrumadora y extremadamente ineficiente; especialmente si queremos aplicar el mismo cambio a todos estos. Imaginemos que tenemos 5 servidores Linux con SSH instalado para gestionarlos remotamente; y tenemos que hacer (por ejemplo) una simple tarea de actualización, junto con añadir un nuevo DNS a/etc/resolv.conf. Ambas tareas son simples, pero no deja de requerir tiempo pues requiere replicarlas en 5 servidores. Es por ello que en ciertos entornos es recomendado tener alguna herramienta de automatización de tareas... Existen varias herramientas, tales como Chef, Puppet o Ansible, pero hoy nos vamos a centrar en la que, en mi opinión, es la que tiene la menor curva de aprendizaje y la que es más sencilla de gestionar: Ansible.

Logo_ansible

Ansible es una herramienta de automatización de tareas que puede mandar comandos a múltiples máquinas ya sea desde la consola a la fuerza, o mediante unos ficheros, cuyo formato es YAML  (conocidos como playbooks), que siguen una estructura muy definida, en la que se establecen todo tipo de comandos, comportamientos, desencadenadores ante ciertos eventos, etc... Lo más común y lo ideal, es trabajar con dichos ficheros, pues se puede dejar todo configurado en estos y luego simplemente con decirle a Ansible que realice las acciones pertinentes que le digan éstos; pero a modo de introducción, es mejor comenzar con lo más básico y realizar tareas sencillas para luego, en caso de ver que la herramienta se adapta a nuestras necesidades, pasar a usar Ansible en profundidad.

Lo primero y necesario para poder gestionar todas las máquinas, es que todas tengan el paquete openssh-server instalado. Esto sí que es necesario hacer a mano. También es necesario tener dicho paquete instalado en la máquina controladora.
apt-get install openssh-server
La necesidad de dicho paquete radica en que todas las comunicaciones se hacen vía SSH, con lo que en caso de tener un firewall entre el controlador que enviará los comandos al resto, y las máquinas, habría que asegurarse de que éste no está bloqueando el puerto correspondiente (por defecto el 22). Aún así con esto no valdría; habría que habilitar el acceso de root por SSH a las otras máquinas; esta parte es subjetiva, ya que dependiendo de qué Linux estemos usando y la versión de éste, podemos tener habilitado el acceso remoto, pero lo ideal es asegurarse... Para ello habría que editar el fichero /etc/ssh/sshd_config y comprobar que el parámetro PermitRootLogin esté puesto a yes:
PermitRootLogin yes
Por supuesto habría que reiniciar el servicio SSH.
/etc/init.d/ssh restart
Ahora bien, hay que pensar en hacer las tareas lo más automatizadas posible, con lo que vamos a usar el método de autenticación de clave pública y privada para evitar tener que introducir la contraseña a cada servidor, ahorrándonos mucho tiempo y pudiendo hacer que las tareas de Ansible se ejecuten sin tener que estar nosotros delante autenticándonos cada vez que se conecte a un nuevo servidor. Esto puede parecer tedioso, pero hay que pensar que es un proceso se hará una sola vez, al igual que la instalación de SSH. Para ello, desde el servidor que usaremos como el controlador, es decir desde el que usaremos para enviar ordenes al resto con Ansible, generaremos un par de claves (una pública y una privada) RSA.
ssh-keygen -t rsa
Es importante matizar que cuando nos pregunten sobre qué contraseña ponerle a la clave privada para poder usarla, no poner ninguna, pues dicha clave privada, en un principio, no va a salir del equipo.
.
Ahora para exportar la clave pública a otro equipo se usaría el comando ssh-copy-id tal que así:

ssh-copy-id root@IP

Por ejemplo, si uno de los equipos tuviese la IP 192.168.1.8 haríamos:
ssh-copy-id root@192.168.1.8
Este comando habría que repetirlo para cada IP que queramos controlar. Obviamente, al copiar la clave pública, el equipo al queramos copiar la clave nos pedirá la clave de root de dicho equipo. Gracias a dicho comando ya podremos entrar a dichos equipos sin necesidad de contraseña, lo cual hará que cualquier tarea sea mucho menos tediosa; ahora bien, faltaría instalar Ansible en el equipo que queremos que "controle" al resto, es decir el equipo desde el que hemos enviado nuestra clave pública al resto. Afortunadamente instalar Ansible es tan sencillo como instalarlo de los repositorios:
apt-get install ansible
Ahora que tenemos Ansible instalado, lo primero que haremos será dirigirnos al fichero /etc/ansible/hosts; dicho fichero sería el encargado de recopilar los diferentes equipos que queremos controlar. En dicho fichero se pueden crear diferentes grupos que agrupan distintos hosts; grupos que se pueden llamar de forma individual, o que también pueden ser llamados todos a la vez.  Cada grupo tendrá la siguiente estructura:

[nombre_grupo]
IP1
IP2
...

Un ejemplo puramente teórico podría ser:
[WEB]
192.168.1.7
192.168.1.8

[ASTERISK]
192.168.1.9
A modo de ejemplo vamos a crear un solo grupo que tendrá la IP 192.168.1.8 dentro del susodicho, pues sabemos que dicha IP tiene nuestra clave pública y que podremos efectuar cualquier acción sobre el equipo con dicha IP sin preocuparnos en introducir la contraseña.
[PRUEBAS]
192.168.1.8
Ahora que tenemos el fichero hosts preparado, vamos a efectuar un par de comando básicos:

El primero sería una simple comprobación del contenido del fichero /etc/resolv.conf de cada equipo controlado por Ansible:
root@debian:~# ansible all -a \ 
"cat /etc/resolv.conf"
192.168.1.8 | SUCCESS | rc=0 >>
nameserver 192.168.1.1
Esto haría que lanzásemos una consulta del contenido del fichero /etc/resolv.conf a cada equipo bajo nuestro control, y que cada uno nos devolviese como respuesta la salida de dicho comando; ahora bien; qué significa la estructura arriba mostrada? Dicha estructura es tan sencilla como:

ansible nombre_grupo -a "comando de Linux"

A la hora de poner el nombre de grupo podemos poner el nombre del grupo al que queremos hacer referencia, como por ejemplo PRUEBAS, o por el contrario, si queremos hacer referencia a todos los grupos habría que simplemente poner el nombre all, tal y como hemos hecho arriba. Es decir que en nuestro anterior comando podríamos haber puesto el nombre del grupo PRUEBAS en vez de all y haber obtenido el mismo resultado. Al igual que podemos haber mandando dicho comando, podemos lanzar cualquier otro comando común de la consola, como podría ser un "ifconfig" o incluso un "apt-get update"; es decir que prácticamente con la prueba de concepto de arriba, podrían mandarse comandos de consola simples a todos los equipos, lo cual de por sí nos puede ahorrar una cantidad de tiempo considerable.

Ansible ofrece más opciones más allá de los comandos de shell, ya que si bien he omitido este concepto a propósito en el primer ejemplo, cuando se mandan ordenes a través de Ansible, se mandan gracias al uso de módulos que son llamados gracias al parámetro -m. La lista de módulos y sus diferentes parámetros es realmente enorme; lista que se pueden encontrar en la página oficial de Ansible como podéis ver en este enlace. Mencionarlos todos es prácticamente imposible, pero hay unos pocos que pueden resultar interesantes de mencionar; en concreto habrían 3 módulos que en mi opinión son interesantes conocer:

  • shell: Este módulo es el usado por defecto. Es decir que no especificar un módulo o poner que se quiere usar este módulo viene a ser lo mismo. Este módulo ejecuta directamente ordenes de shell.
  • service: Este es un módulo muy interesante, ya que se encarga de comprobar el estado de los servicios y manipularlos. Puede pararlos, arrancarlos e incluso añadirlos y quitarlos del arranque... En definitiva, es un módulo muy interesante a la hora de mantener servidores.
  • apt: Este módulo se dedica a usar apt para realizar actualizaciones de la caché, instalaciones, actualizaciones y eliminaciones de paquetes. 

El primer módulo ya hemos visto como usarlo y que simplemente requiere tener nociones de la shell, pero ¿Cómo usamos los módulos service y apt?

El módulo service tiene tres argumentos que podrían considerarse como los más importantes: El nombre del servicio, mediante el argumento name, el estado en el que queremos que esté mediante el comando state, y si queremos que se inicio no durante el proceso de arranque mediante el comando enabled.  Los estados pueden ser: started, stopped, restarted y reloaded; mientras que la habilitación del arranque durante el inicio sería: yes o no.

La mejor forma de entender los argumentos es viendo el uso de estos:

Imaginemos que queremos parar el servicio ntp en todos los equipos que controlamos; el comando que lograría dicho resultado sería:
root@debian:~# ansible all -m service -a \
"name=ntp state=stopped"
192.168.1.8 | SUCCESS => {
    "changed": true,
    "name": "ntp",
    "state": "stopped",
Cada comando que lancemos que a cada máquina nos devolverá una serie de líneas, entre las cuales nos importará ver, por un lado que el comando se ha enviado exitosamente, lo cual se deduce al ver que devuelve un SUCCESS, y por otro lado hay una línea en la que veremos que dirá "changed"; en caso de dar como respuesta true, significará que antes se encontraba parado, mientras que al decir false nos estaría diciendo que ya se encontraba parado antes de que nosotros se lo ordenásemos. Otro ejemplo más completo, aprovechando que el servicio ntp se encuentra parado podría ser:
root@debian:~# ansible all -m service -a \
"name=ntp state=started enabled=yes"
192.168.1.8 | SUCCESS => {
    "changed": true,
    "enabled": true,
    "name": "ntp",
    "state": "started",
En este caso no solo le habríamos ordenado que arrancase, sino que además le estaríamos ordenando que se iniciase durante el proceso de arranque el equipo...  Este ejemplo se ha usado para el servicio NTP pero podría extrapolarse a cualquier otro sin problema alguno.

Veamos ahora el módulo apt. Este módulo funciona de la misma forma que el uso habitual de apt-get, con la diferencia que desde aquí podremos controlar pequeños detalles como que por ejemplo tengamos un paquete concreto siempre con la última versión. Este módulo tiene muchos parámetros, pero los más importantes serían: name, cuya utilidad es la misma que la usada en el módulo de service y que está presente en la gran mayoría de los módulos, state, que en este caso tiene unos estados diferentes a los del módulo service, y update_cache, que sería un equivalente al comando apt-get update. Los estados en este caso serían: latest (última versión del paquete), absent, present y  build-dep. El último estado simplemente comprobaría que el paquete X tiene todas las dependencias necesarias instaladas.

A sabiendas de dichos parámetros, podemos jugar con el módulo apt para, por un lado nos actualice la caché de los repositorios y que por otro lado nos instale (si no tenemos instalada ya) la última versión de NTP.
root@debian:~# ansible all -m apt -a \
"name=ntp update_cache=yes state=latest"
192.168.1.8 | SUCCESS => {
    "cache_update_time": 1515448761,
    "cache_updated": true,
    "changed": false
}
Gracias a dicho módulo podemos mantener actualizados todos los equipos sin tener que ir uno por uno realizando dicha tarea.

Existen muchos más módulos cuya importancia varía dependiendo de las herramientas que estemos usando, pero estas tres serían las más básicas y con las que se podrían realizar las tareas más sencillas sin necesidad de introducirse en el mundo de los playbooks.

Como podéis observar el uso de Ansible puede ayudar muchísimo en la administración de varios equipos. Cierto es que en este caso se ha interactuado con uno solo y que hacer la misma tarea directamente en dicho equipo habría sido más sencillo ¿Pero y si fuesen 5, o 10, o incluso 100? Esta herramienta brilla en dicho tipo de entornos y es donde realmente se le saca partido.

Espero que con este post hayáis podido ver las posibilidades que ofrece Ansible y las ventajas que puede ofrecer para automatizar ciertas tareas y ahorrarnos horas de tareas repetitivas.

Saludos.