LXC: entorno de prueba para Ghost y contenedores sin privilegios

Si recordáis mi anterior post creamos un par de contenedores para testear el rendimiento. Se me olvidó mencionar que todo el proceso lo hicimos como usuario root con lo que perdíamos un poco de seguridad, uno de los pilares para los que se creó esta tecnología. Vamos a mejorar este extremo explicando cómo crear contenedores sin privilegios.

Según nos cuentan en este post, el entorno por defecto en Ubuntu para contenedores sin privilegios está mejor preparado que en Debian. Aunque seguiremos el mismo esquema, intentaremos configurar lo mismo en ambas distribuciones.

Si intentamos crear un contenedor como un usuario normal en cualquiera de las dos distribuciones obtenemos lo siguiente (o con cifras parecidas):

$ lxc-create -t download -n ubuntu-ghost-test
lxc-create: conf.c: chown_mapped_root: 3340 No mapping for container root  
lxc-create: lxccontainer.c: do_bdev_create: 1047 Error chowning /home/bbc/.local/share/lxc/ubuntu-ghost-test/rootfs to container root  
lxc-create: conf.c: suggest_default_idmap: 4444 You must either run as root, or define uid mappings  
lxc-create: conf.c: suggest_default_idmap: 4445 To pass uid mappings to lxc-create, you could create  
lxc-create: conf.c: suggest_default_idmap: 4446 ~/.config/lxc/default.conf:  
lxc-create: conf.c: suggest_default_idmap: 4447 lxc.include = /etc/lxc/default.conf  
lxc-create: conf.c: suggest_default_idmap: 4448 lxc.id_map = u 0 1279648 65536  
lxc-create: conf.c: suggest_default_idmap: 4449 lxc.id_map = g 0 1279648 65536  
lxc-create: lxccontainer.c: do_lxcapi_create: 1511 Error creating backing store type (none) for ubuntu-ghost-test  
lxc-create: lxc_create.c: main: 318 Error creating container ubuntu-ghost-test  

Como véis en la propia ejecución lxc nos sugiere la solución. Para llevarla a cabo nos crearemos el archivo ~/.config/lxc/default.conf con el siguiente contenido, pero vamos a cambiar un poco los IDs (ya lo explicaremos más abajo):

lxc.include = /etc/lxc/default.conf  
lxc.id_map = u 0 100000 65536  
lxc.id_map = g 0 100000 65536  

Ahora ya sólo tenemos que lidiar con el siguiente error:

$ lxc-create -t download -n ubuntu-test
unshare: Operation not permitted  
read pipe: Permission denied  
lxc-create: lxccontainer.c: do_create_container_dir: 978 Failed to chown container dir  
lxc-create: lxc_create.c: main: 318 Error creating container ubuntu-test  

Esto último sólo nos ocurre en Debian, parece lógico que Ubuntu esté mejor preparada por defecto para trabajar con LXC ya que el proyecto está prácticamente bajo el paraguas de Canonical. Volviendo a Debian, ejecutando los siguientes comandos podremos crear el contenedor sin problemas:

# echo 1 > /sys/fs/cgroup/cpuset/cgroup.clone_children
# echo 1 > /proc/sys/kernel/unprivileged_userns_clone

Aunque esto es sólo una solución temporal, más adelante veremos cómo establecer un remedio duradero, primero vamos con el grueso de las configuraciones.

Tanto en Ubuntu como en Debian recrearemos el esqueleto de directorios con el usuario que hayamos elegido para crear nuestros contenedores sin privilegios. Como bien hemos podido leer en el post del blog ./myles.sh, para este paso nos apoyaremos a su vez en este otro post creado por uno de los desarrolladores de LXC. Según él, la estructura de directorios debe ser la siguiente:

/etc/lxc/lxc.conf => ~/.config/lxc/lxc.conf
/etc/lxc/default.conf => ~/.config/lxc/default.conf
/var/lib/lxc => ~/.local/share/lxc
/var/lib/lxcsnaps => ~/.local/share/lxcsnaps
/var/cache/lxc => ~/.cache/lxc

Con esta información ya podemos crearlos:

$ mkdir -p ~/.config/lxc
$ touch ~/.config/lxc/{lxc,default}.conf
$ mkdir -p ~/.local/share/{lxc,lxcsnaps}
$ mkdir -p ~/.cache/lxc

El siguiente paso, sólo necesario en Debian, consistirá en activar en el kernel la capacidad para un usuario sin privilegios de crear namespaces, según nos cuentan en el mismo post, esta funcionalidad se desactivó en la versión 3.16, aunque estaba disponible desde la versión 3.12. Esta condición no tiene mayor problema, simplemente la activaremos creando el fichero /etc/sysctl.d/80-lxc-userns.conf con el siguiente contenido:

# Allow creation of user namespaces to unprivileged users
kernel.unprivileged_userns_clone = 1  

Ahora recargamos la configuración de sysctl y comprobamos que ha surtido efecto:

# sysctl --system
* Applying /etc/sysctl.d/30-postgresql-shm.conf ...
* Applying /etc/sysctl.d/80-lxc-userns.conf ...
kernel.unprivileged_userns_clone = 1  
* Applying /etc/sysctl.d/99-sysctl.conf ...
* Applying /etc/sysctl.conf ...
# cat /proc/sys/kernel/unprivileged_userns_clone
1  

El resultado final de 1 nos indica que todo ha funcionado correctamente. Ahora vamos con los IDs de usuario. Como recordaréis de un poco más arriba, este problema ya lo habíamos solucionado, pero apoyándonos en los números que nos sugirió el propio sistema, vamos a establecer subids globales para ese usuario en concreto. Creo que esto no es estrictamente necesario pero intuyo que en el futuro puede tener otras implicaciones que ahora quizás se nos escapen por lo que he optado por añadirlo en la receta.

Pues bien, para añadir los límites que ya teníamos fijados en la configuración local de nuestro usuario sin privilegios necesitamos un paquete extra que incluso puede que esté ya instalado:

# apt install uidmap

Ahora ya sólo necesitamos crear estos subíndices en el sistema:

# usermod --add-subuids 100000-165536 <nombre_usuario>
# usermod --add-subgids 100000-165536 <nombre_usuario>

Con esto conseguimos que los procesos que corran dentro de los contenedores sin privilegios tengan sus UIDs y GIDs remapeados del típico rango 0-65536 al 100000-165536. Así el anfitrión sabrá que estos procesos pertenecen al usuario que está lanzando estos contenedores.

En este momento ya podremos crear nuestro contenedor sin privilegios también en Debian con lo que estaremos un poco más seguros con nuestros despliegues masivos de granjas de servidores empaquetados.

Como vimos en el post anterior el acceso a la red desde dentro de los contenedores es muy sencillo de configurar en el caso de contenedores ejecutados por el usuario root, pues bien, aprovechando la ocasión vamos a dar un paso más allá e intentaremos configurar ésta mediante uno de los bridges que creamos para KVM, así los contenedores y las máquinas virtuales estarán en las mismas condiciones y podremos compararlas sobre la misma arena.

A partir de ahora los métodos son comunes tanto para Ubuntu como para Debian. Suponiendo que ya tenemos un bridge configurado correctamente, como ya explicamos en mi post inicial sobre KVM, vamos a utilizarlo en nuestros contenedores.

En Ubuntu tendremos que eliminar la configuración por defecto que nos creó para LXC, esto no es necesario pero nos evitará ruido en las configuraciones de red. Primero, como usuario root editaremos el archivo /etc/default/lxc-net para cambiar la variable USE_LXC_BRIDGE y ponerla a false:

# This file is auto-generated by lxc.postinst if it does not
# exist.  Customizations will not be overridden.
# Leave USE_LXC_BRIDGE as "true" if you want to use lxcbr0 for your
# containers.  Set to "false" if you'll use virbr0 or another existing
# bridge, or mavlan to your host's NIC.
USE_LXC_BRIDGE="false"  
[...]

Con esto conseguimos que los contenedores no utilicen por defecto la interfaz definida en ese mismo fichero. En Debian este paso no es necesaio ya que esta funcionalidad está desactivada de inicio. Lo siguiente, en ambas distribuciones, es hacer que usen nuestro bridge ya creado, para ello editamos el fichero ~/.config/lxc/default.conf y añadimos lo siguiente:

[...]
# To use the bridges already configured
lxc.network.type = veth  
lxc.network.link = br0  
lxc.network.name = eth0  
lxc.network.flags = up  

Ahora bien, como esta configuración pertenece sólo a nuestro usuario sin privilegios debemos decirle al sistema que tiene permiso para utilizarlo. Editaremos el archivo /etc/lxc/lxc-usernet y añadiremos la siguiente línea (en el caso de Debian debemos incluso crearlo):

# USERNAME                   TYPE    BRIDGE    COUNT
<usuario_sin_privilegios>    veth    br0       2  

Una vez que tenemos el contenedor preparado ya podremos lanzarlo mediante el comando lxc-start y nos conectaremos directamente como root con lxc-attach:

$ lxc-start -n ubuntu-test
$ lxc-attach -n ubuntu-test
#/

Hemos detectado unas diferencias en la configuración del contenedor en Debian respecto a Ubuntu, parece ser que las variables de entorno que hereda el terminal al realizar un attach no son las mismas cuando el anfitrión es uno u otro. En Ubuntu, partiendo de este terminal podemos realizar ya todas las tareas normales de un servidor pero desde Debian necesitamos establecer correctamente la variable de entorno PATH:

#/ export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
#/ apt-get install openssh-server

Con este paquete, y si hemos añadido un usuario nuevo, ya podremos conectarnos desde fuera del contenedor y trabajar en él con total normalidad. Desde el terminal que nos proporciona el comando lxc-attach ejecutando un simple ifconfig podréis saber que ip tiene asignado para entrar por ssh.

Yo voy a recrear mi máquina de test del propio Ghost para probar las actualizaciones antes de hacerlo en el servidor principal. Lo único que debemos hacer es seguir los pasos que ya expliqué en su día una vez instalado lo básico. Como parece ser que node.js ahora lo empaquetan para Debian y derivados en otro repositorio he decidido hacer un post nuevo con una explicación actualizada para Ubuntu 16.04 ya que el servidor sobre el que está corriendo el blog, ya tiene la nueva y flamante versión LTS.

A la espera de este nuevo post os dejo con los chicos malos del barrio, como diría el gran Andrés Montes, los neoyorquinos Beastie Boys y su himno Fight For Your Right (To Party) del primer disco 100% Rap de su repertorio Licensed To Ill.

Party!!!