The Perfect Server: Modernizando la infraestructura

Hoy vamos a darle una vuelta a nuestro silencioso servidor de casa. Tengo montados varios servicios en esta máquina pero ya sabéis de mi afán por tener las Debian personales en la versión inestable y actualizadas al día. Esto no se lleva muy bien con ciertos servicios que comparto con el resto de la familia. Entre ellos tengo montado un Owncloud para compartir fotos de eventos y demás documentos que puedan interesar a varios de nosotros. Teniendo en cuenta que este proyecto tira de PHP y sabiendo que ya ha aparecido la versión 7.0 os podéis imaginar que cuando se pasó de la versión 5.0 a esta última sufrí bastante para que todo volviese a funcionar.

Por esta razón me he decidido a cambiar un poco el planteamiento y vamos a empezar por este servicio, de paso vamos a ver si aprendemos un poco de automatización.

Estos últimos meses en el trabajo he estado liado con la securización de nuestro datacenter y su adecuación al estándar de seguridad PCI-DSS, los que hayáis oído hablar de esto último ya habréis echado mano de algún tranquilizante que tengáis cerca, pero es algo por lo que hay que pasar alguna vez en la vida. Con lo que he aprendido por el camino me he dado cuenta de que hay ciertas prácticas en los sistemas a las que no estaba prestando demasiada atención. Vamos a aplicar algo tan sensato como separar la infraestructura en capas pero juntando nuestros conocimientos en contenedores y usando como conejillo de indias mi almacén de fotos familiares.

Lo primero que quiero conseguir con esto es estabilizar este proyecto en mi servidor separándolo del sistema principal, de paso aprenderemos un montón de cosas nuevas. La idea inicial es crear los contendores necesarios para separar en capas la pila de servicios que se necesitan para montar un Owncloud, aunque en mi caso también voy a migrar a Nextcloud, un fork de este último, pero no os preocupéis, aquí os daré los pasos necesarios para montar uno limpio, la migración me la guardo para divertirme yo sólo.

Como todos sabemos, para montar un servicio web necesitamos, un servidor web, evidentemente, una base de datos, generalmente, espacio para guardar ficheros, un firewall, quizás, puede que también un servidor de caché, y algunas cosas más. Pues bien, en el caso del proyecto Nextcloud necesitaremos un servidor web, hemos elegido Nginx ya que lo hemos utilizado en el propio blog, una base de datos, MariaDB y un almacén de datos en el que guardar nuestros preciados documentos y fotografías. Como firewall utilizaremos nuestro propio router de casa, ya securizaremos esto en serio en el futuro.

Consultando diversas fuentes parece que no es trivial separar Nginx en un contenedor y la ejecución del código PHP en otro por lo que para un primer montaje tendremos el servidor web en un contenedor, el servidor de bases de datos en otro, pero los archivos tendremos que alojarlos en el mismo contenedor en el que montemos Nginx. Cuando vayamos añadiendo funcionalidades a nuestro servidor principal de casa veremos si nos conviene investigar un poco más para separar estas capas.

Manos a la obra. Vamos a preparar un par de contenedores sin privilegios en una máquina de prueba y otro par en nuestro servidor principal como ya explicamos en un post anterior, luego crearemos unas recetas con Ansible y así podremos reproducir nuestra infraestructura en cualquier servidor con capacidad de levantar contenedores:

$ lxc-create -t download -n ubuntu-xenial-nginx -- -d ubuntu -r xenial -a amd64
[...]

El problema para trabajar con contenedores es la configuración inicial ya que estos se crean, por motivos de seguridad perfectamente comprensibles, sin usuarios y con la contraseña de root deshabilitada. Seguro que tocando directamente el template que nos crea un contenedor con ubuntu podríamos realizar ahorrarnos estos pasos iniciales, pero por ahora yo he optado por esta solución.

En cada contenedor necesitaremos un usuario con privilegios para poder ejecutar tareas de administración y para conectarnos cómodamente a él tendremos que añadir nuestra clave pública al fichero ~/.ssh/authorized_keys del propio usuario dentro del contenedor. Vamos a borrar el usuario por defecto, que aunque no tenga un password definido no deja de ser un usuario sobrante en el sistema:

$ lxc-start -n ubuntu-xenial-nginx
$ lxc-attach -n ubuntu-xenial-nginx -- /usr/sbin/deluser ubuntu

Ahora crearemos nuestro usuario y lo añadiremos al grupo sudo para que pueda ejecutar las tareas de administración:

$ lxc-attach -n ubuntu-xenial-nginx -- /usr/sbin/adduser <username>
Adding user `<username>' ...  
Adding new group `<username>' (1000) ...  
Adding new user `<username>' (1000) with group `<username>' ...  
Creating home directory `/home/<username>' ...  
Copying files from `/etc/skel' ...  
Enter new UNIX password: *********  
Retype new UNIX password: *********  
passwd: password updated successfully  
Changing the user information for <username>  
Enter the new value, or press ENTER for the default  
    Full Name []: 
    Room Number []: 
    Work Phone []: 
    Home Phone []: 
    Other []: 
Is the information correct? [Y/n] Y  
$ lxc-attach -n ubuntu-xenial-nginx -- /usr/sbin/adduser <username> sudo
Adding user `<username>' to group `sudo' ...  
Adding user <username> to group sudo  
Done.  

Ahora nos faltaría instalar el servidor de ssh dentro del contenedor para poder conectarnos mediante este protocolo, pero para probar que podemos ejecutar comandos mediante este nuevo usuario lo haremos conectándonos a través de una consola:

$ lxc-console -n ubuntu-xenial-nginx

Connected to tty 1  
                  Type <Ctrl+a q> to exit the console, <Ctrl+a Ctrl+a> to enter Ctrl+a itself

Ubuntu 16.04.1 LTS ubuntu-xenial-nginx pts/0

ubuntu-xenial-nginx login: <username>  
Password: ********  
Welcome to Ubuntu 16.04.1 LTS (GNU/Linux 4.9.0-1-amd64 x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

The programs included with the Ubuntu system are free software;  
the exact distribution terms for each program are described in the  
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by  
applicable law.

To run a command as administrator (user "root"), use "sudo <command>".  
See "man sudo_root" for details.

<username>@ubuntu-xenial-nginx:~$ sudo apt update  
[sudo] password for <username>: ********
Hit:1 http://archive.ubuntu.com/ubuntu xenial InRelease  
[...]
@ubuntu-xenial-nginx:~$ sudo apt install openssh-server python-simplejson
Reading package lists... Done  
[...]

Este último paquete es necesario para que podamos comunicarnos con los distintos contenedores mediante la herramienta de automatización Ansible como veremos un poco más adelante.

Pulsando Ctrl+a y después la tecla q saldremos de esta sesión y ya podremos probar a conectarnos a la ip que le haya asignado nuestro servidor de dhcp local. Si no sabemos cuál es, ejecutando lo siguiente podremos averiguarlo:

$ lxc-ls -f
NAME                STATE   AUTOSTART GROUPS IPV4          IPV6  
ubuntu-xenial-nginx RUNNING 0         -      192.168.1.134 -  
$ ssh <username>@192.168.1.134

Ahora, la forma más fácil de añadir nuestra clave pública es utilizando el comando ssh-copy-id:

$ ssh-copy-id <username>@192.168.1.134

Ahora, si probáis a conectaros mediante ssh veréis que no necesitáis introducir la contraseña. Si no tenéis una clave pública, la forma de crearla es muy sencilla:

$ ssh-keygen -t rsa

Bien, una vez que tenemos nuestro contenedor preparado podemos clonarlo para tener preparado también el contenedor de nuestra base de datos:

$ lxc-stop -n ubuntu-xenial-nginx 
$ lxc-copy -n ubuntu-xenial-nginx -N ubuntu-xenial-mariadb
$ lxc-start -n ubuntu-xenial-nginx 
$ lxc-start -n ubuntu-xenial-mariadb 
$ lxc-ls -f
NAME                  STATE   AUTOSTART GROUPS IPV4          IPV6  
ubuntu-xenial-mariadb RUNNING 0         -      192.168.1.137 -  
ubuntu-xenial-nginx   RUNNING 0         -      192.168.1.134 -  

Y también podréis conectaros directamente a este nuevo contenedor sin problemas. Seguramente haya una forma más cómoda y automática de crear los contenedores pero como de momento vamos a manejar unos pocos no me meteré a investigar cómo editar los templates disponibles.

Ahora vamos a ver un poco de automatización con Ansible. Por ahora no he profundizado mucho, primero vamos a crearnos un catálogo de servidores sobre los que actuar y después ejecutaremos algunos comandos sobre ellos, el siguiente paso espero que sea crear una receta, o playbook, completa para levantar un contenedor con el servidor web Nginx y otra con el servidor de bases de datos MariaDB.

Lo primero, evidentemente, es instalar esta herramienta en el equipo desde el que queramos controlar nuestros contenedores:

# apt install ansible

El inventario de equipos que utilizaremos es un simple archivo de texto con nuestros servidores agrupados mediante categorías, con el siguiente ejemplo lo veremos fácilmente:

[webserver-test]
ubuntu-xenial-nginx-test ansible_host=192.168.1.202  
[webserver]
ubuntu-xenial-nginx ansible_host=192.168.1.200

[database-test]
ubuntu-xenial-mariadb-test ansible_host=192.168.1.203  
[database]
ubuntu-xenial-mariadb ansible_host=192.168.1.201

[nextcloud:children]
webserver-test  
webserver  
database-test  
database  

En este caso nos hemos creado cuatro grupos, que en realidad sólo tienen un miembro y luego un grupo general por si queremos ejecutar tareas comunes a todos nuestros contenedores. Para empezar vamos a ver un ejemplo sencillo, ejecutaremos un echo para mostrar cómo se puede utilizar Ansible con comandos simples. Suponiendo que el fichero anterior lo hemos llamado inventory_nextcloud debemos invocar el siguiente comando:

$ ansible nextcloud -i inventory_nextcloud -a "/bin/echo hello world!"
ubuntu-xenial-nginx-test | SUCCESS | rc=0 >>  
hello world!

ubuntu-xenial-mariadb-test | SUCCESS | rc=0 >>  
hello world!

ubuntu-xenial-nginx | SUCCESS | rc=0 >>  
hello world!

ubuntu-xenial-mariadb | SUCCESS | rc=0 >>  
hello world!  

El primer argumento, netxcloud, indica el grupo de máquinas que hemos expresado en nuestro inventario. Con el modificador -i indicamos el fichero de inventario y con el modificador -a expresamos la orden a ejecutar. Como podéis ver es muy sencillo aunque seguro que os estaréis preguntando cómo podemos ejecutar órdenes más serias, como una actualización del sistema, por ejemplo. Pues bien, en párrafos anteriores hemos configurado nuestro usuario para que pertenezca al grupo sudo pero si intentamos ejecutar una orden con privilegios veremos cómo el sistema no nos lo permite. La opción que he elegido en principio es permitir que este usuario pueda ejecutar estos comandos sin necesidad de introducir una contraseña. Tendremos que cambiar el archivo de configuración de sudoers en la entrada correspondiente al grupo sudo y dejarla como sigue:

%sudo   ALL=(ALL:ALL) NOPASSWD: ALL

Esta edición se realiza mediante la orden visudo. Sé que hay formas mejores de hacerlo, como por ejemplo configurar un almacén de claves centralizado que utilice el propio Ansible, pero por ahora nos conformaremos haciendo este cambio en nuestros cuatro contenedores. Cuando entremos en las playbooks lo resolveremos de esa otra forma.

Una vez que tenemos estos privilegios ya podemos lanzar una actualización de paquetes, como haríamos en caso de tenerlo programado en nuestro día a día para instalar los parches de seguridad de Ubuntu. En mi caso ejecuto tres comandos, el primero actualiza los índices, el segundo ejecuta las actualizaciones disponibles y el tercero limpia la caché de paquetes para no quedarnos con archivos innecesarios en el sistema:

$ ansible all -i inventory_nextcloud -a "/usr/bin/apt update" --become
$ ansible all -i inventory_nextcloud -a "/usr/bin/apt dist-upgrade -y" --become
$ ansible all -i inventory_nextcloud -a "/usr/bin/apt clean" --become

Y esto es sólo la punta del iceberg, en el siguiente post intentaremos crearnos una playbook que nos levante un Nginx en el contenedor designado, un MariaDB en otro, desplegando Nextcloud en ellos para poder compartir documentos con nuestra familia y amigos. En estos cuatro contenedores establecí unas ips fijas para poder definirme fácilmente los servidores en el inventario, si vosotros tenéis un servidor de dns que se ocupe de estos menesteres os podéis ahorrar este paso en la configuración.

También he de deciros que en ningún momento he especificado el usuario con el que quería ejecutar los comandos en el contenedor utilizando el modificador -u, ya que los he creado con el mismo nombre que tiene el usuario con el que estoy lanzando los comandos de Ansible, si vuestro caso es diferente tenedlo en cuenta para todas estas configuraciones.

Espero que con este post os haya picado el gusanillo y empecéis a automatizar todas esas tediosas tareas que os quitan valiosos minutos de vuestras vidas.

Para despedirme os voy a aconsejar una canción de Olive, una banda del norte de inglaterra, que hacían un trip-hop que me trae muy buenos recuerdos de esos años, mediados de los noventa. Seguramente en aquella época se empezó a poner de moda el aceite de oliva en Gran Bretaña porque bautizaron a su álbum Extra Virgin en el que aparece este melocotonazo You're Not Alone.

Disfrutad!