Danielside

informática softwarelibre divagaciones música

Contenedor telegram con Buildah y Podman (II)

Casi 2 meses después de la primera entrega he ido avanzando en mi conocimiento de Podman y Buildah y ya tengo un contenedor casi completo para sustituir al servicio de puente Telegram, que se está ejecutando actualmente en un contenedor LXC.

La versión en contenedor Podman es ideal para este servicio. Según voy sabiendo más, no creo que los símiles que se han ido usando para explicar qué son los contenedores hayan sido los más adecuados. Circula por ahí la idea de que son como «máquinas virtuales ligeras» o «virtualización de los pobres», pero esto es una verdad a medias.

En el mundo de LXC (sistema con el cual tengo ahora mismo montado mi servidor VPS) sí es cierto que un contenedor es prácticamente una máquina virtual con un sistema operativo completo: todas las librerías, su sistema de init, todos los programas de la instalación por defecto del sistema, etc.

Pero los contenedores Podman/Docker hay que verlos como un aislamiento de procesos. Es una especie de «empaquetamiento» (esto sería la imagen) de un proceso junto con todas sus librerías y configuraciones, y nada más; es decir no debes tener en el contenedor editores de texto, herramientas auxiliares de depuración ni nada por el estilo y ni siquiera debes ser capaz de conectarte (o no deberías tener la necesidad) por ssh o cualquier otro medio a la «consola» de ese contenedor. La idea es que jamás tenga que hacer ningún ajuste manual, solo en la imagen. Piensa que cuando tienes un proceso corriendo en tu ordenador (por ejemplo Prosody) tampoco «te conectas a ese proceso» para depurar ni inspeccionar nada. No se si me explico bien pero creo que eso se acerca a la idea de lo que debe ser un contenedor.

Por eso el cliente Telegram de consola era un candidato ideal. Como es muy antiguo y no conseguía que compilase en nada más moderno que Debian Jessie (Debian 8), tenía que empaquetar el binario compilado de telegram-cli junto con la imagen mínima de Debian Jessie, más las configuraciones, en un «contenedor», o más bien en una «imagen de un contenedor». El contenedor es lo que se ejecuta a partir de la imagen creada. Veamos como va el código de Buildah:

#!/usr/bin/env bash

set -o errexit

## Configuración
telegram_user=telegramd
telegram_user_uid=500
telegram_group=$telegram_user
telegram_home=/home/$telegram_user/tg
telegram2xmpp_home=/home/$telegram_user/telegram2xmpp
telegram_media=/home/$telegram_user/media
telegram_bin=/home/$telegram_user/bin
telegram_config_home=/home/$telegram_user/.telegram-cli

timestamp=`date +%Y%m%d%H%I`

container=$(buildah from debian:jessie)
buildah config --label maintainer="Danielside <danielside@posteo.net>" $container

## Dependencias, configuración e instalación de Telegram
buildah run $container apt update
buildah run $container apt install -y apt-utils
buildah run $container apt install -y --no-install-recommends ca-certificates git \
	apt-utils libreadline-dev libconfig-dev libssl-dev lua5.2 liblua5.2-dev \
	libevent-dev libjansson-dev libpython-dev build-essential git

buildah run $container groupadd --gid $telegram_user_uid $telegram_user
buildah run $container useradd --uid $telegram_user_uid --gid $telegram_user_uid \
       --create-home --shell /bin/bash $telegram_user

buildah run $container git clone --recursive https://github.com/vysheng/tg.git $telegram_home

buildah config --workingdir $telegram_home $container

buildah run $container ./configure

buildah run $container make

buildah run $container mkdir $telegram_config_home
buildah run $container mkdir $telegram_media

buildah copy $container auth $telegram_config_home/auth
buildah copy $container secret $telegram_config_home/secret

buildah run $container chown -R $telegram_user.$telegram_group $telegram_config_home
buildah run $container chown -R $telegram_user.$telegram_group $telegram_media

## Configuración de extensiones de Telegram > XMPP
buildah run $container git clone https://gitlab.com/danielside/telegram2xmpp.git $telegram2xmpp_home
buildah config --workingdir $telegram2xmpp_home $container
buildah run $container git checkout -b feature/inside_podman
buildah run $container git pull origin feature/inside_podman

buildah copy $container tg2xmpp_conf.lua $telegram2xmpp_home/tg2xmpp_conf.lua
buildah run $container chown -R $telegram_user.$telegram_group $telegram2xmpp_home

buildah run $container apt install -y --no-install-recommends sendxmpp

buildah copy $container sendxmpprc /home/$telegram_user/.sendxmpprc
buildah run $container chown $telegram_user /home/$telegram_user/.sendxmpprc
buildah run $container chmod 700 /home/$telegram_user/.sendxmpprc

## Scripts de arranque
buildah run $container mkdir $telegram_bin

buildah copy $container script/arrancar_telegram_debug.sh $telegram_bin/arrancar_telegram_debug.sh
buildah copy $container script/arrancar_telegram.sh $telegram_bin/arrancar_telegram.sh

buildah run $container chmod +x $telegram_bin/arrancar_telegram.sh

buildah run $container chown -R $telegram_user.$telegram_group $telegram_bin

buildah run $container apt-get autoclean
buildah run $container apt-get autoremove

## Punto de entrada y usuario
buildah config --workingdir /home/$telegram_user $container
buildah config --user $telegram_user $container
buildah config --entrypoint $telegram_bin/arrancar_telegram.sh $container

buildah commit --format docker $container telegram:$timestamp

Una vez construida la imagen ejecutando este script, la creación de un contenedor basado en la misma, ha quedado de la siguiente manera:

podman run -d --cap-add NET_RAW \
              --network slirp4netns:allow_host_loopback=true \
              -v/home/daniel/volumes/telegram:/mnt/data \
              --publish 2392:2392 \
              --name telegram \
              localhost/telegram:202110061010

Como veis hay bastante tela que cortar desde la primera versión, vamos por partes.

Capacidades de un contenedor

Los usuarios sin privilegios de Linux (aquéllos que no son root) tienen restringido lo que pueden hacer. Cuando se usan los contenedores como root y además dentro eres root, no hay ningún problema. Pero como esto es una llamada al desastre en términos de seguridad, seremos usuarios sin privilegios tanto dentro como fuera.

Lo que se puede hacer o no en un contenedor rootless se especifica en las capabilities de Linux. Las que tiene asignadas un contenedor en ejecución se pueden ver con podman inspect contenedor buscando «EffectiveCaps» y «BoundingCaps».

En este caso añado CAP_NET_RAW porque es importante para establecer sockets, y sendxmpp necesita acceder a sockets para mandar los mensajes.

Montar volúmenes

Uno de mis propósitos era montar un entorno completamente rootless, es decir:

  • En el contexto del host, los contenedores se ejecutan como un usuario normal (esto es lo que actualmente no puede hacer Docker).
  • En el contexto del contenedor, el proceso principal también se ejecuta como un usuario sin privilegios (que no sea root)

Es la forma más segura de aislar los procesos. Si algo «escapa» del contenedor, solo es un usuario no privilegiado en el contexto del host; al mismo tiempo en el contexto del contenedor, nunca debemos olvidar que el kernel es compartido y por tanto si se es root dentro del contenedor tienes acceso al kernel de la máquina.

Podman ha realizado un gran trabajo para que los contenedores rootless funcionen bien, pero para montar un volumen (algo así como un directorio compartido entre el contenedor y el host) hay que hacer un poco de preparación previa.

Para ello nos vamos a fijar por un lado en telegram_user_uid=500 del script de construcción de la imagen.

Los contenedores modernos existen porque el kernel de Linux proporciona -entre otras características- los espacios de nombres (namespaces). Los espacios de nombres sirven para que un conjunto de procesos de la máquina vea una serie de recursos mientras que otros procesos vean unos recursos diferenciados. Podman crea un espacio de nombres en el espacio de usuario para los contenedores.

Un símil -un poco burdo- para los espacios de nombres podrían ser las letras o números de puerta de un edificio. Supongamos que un amigo te dice que vive en la letra C, tu primera pregunta sería ¿en qué planta? Cada planta es un espacio de nombres diferenciado y esos nombres son las letras A, B, C, D, etc. Si no existieran las plantas, esos nombres colisionarían. Además, hay edificios en los que en algunas plantas hay letras A, B, C y en otros (por ejemplo áticos) solo hay A y B. Es decir, el conjunto de nombres presentes en cada espacio de nombres no tiene porque ser idéntico al del resto de espacios de nombres.

Pues bien, ese uid 500 no es un usuario real del host, es un id de usuario que vive solo en el espacio de nombres de Podman. Linux realiza un mapeo entre los ids de usuario y de grupo reales (de la máquina) y los ids de otros espacios de nombres. Consulta tus ficheros /etc/subuid y /etc/subgid. Ejemplo:

daniel:100000:65536
side:165536:65536

Veamos un momento como deben quedar los permisos de /home/daniel/volumes/telegram, que es el directorio del host que se va a montar en /mnt/data del contenedor, para que tanto el contenedor ejecutándose con usuario telegramd (uid 500) sea capaz de escribir y al mismo tiempo Prosody desde el lado del host sea capaz tanto de leer como de escribir:

$ ls -ald telegram/
drwxr-xr-x 2 100499 prosody 4096 oct  5 15:04 telegram/

¿Qué **** es ese 100499?

Para verlo vamos a olvidarnos por un momento de este directorio y vamos a crear otro desde cero:

$ cd ~/volumes
$ mkdir telegram2
$ ls -ald telegram2
drwxr-xr-x 2 daniel daniel 4096 oct  6 11:44 telegram2

Nada fuera de lo ordinario. Pero si lo montamos así, el contenedor no va a tener permisos (si el usuario que corre dentro del contenedor fuera root, sí tendría).

Para saber cómo vería el contenedor este directorio (montado dentro en /mnt/data) debemos sumergirnos en su espacio de nombres mediante podman-unshare:

$ podman unshare ls -ald telegram2
drwxrwxr-x 2 root nogroup 4096 oct  6 11:44 telegram2

Ahí podemos ver como dentro del contenedor, el directorio estaría montado como propietario root, por tanto telegram2 no podría escribir. De forma que hay que cambiarle los permisos en el espacio de nombres correcto:

$ podman unshare chown 500 telegram2
$ ls -ald telegram2
drwxr-xr-x 2 100499 cirro 4096 oct  6 13:24 telegram2
$ podman unshare ls -ald telegram2
drwxr-xr-x 2 500 root 4096 oct  6 13:24 telegram2

Ahora vemos que desde el host queda ese extraño 100499 pero desde el contenedor se ve como propietario 500 (el identificador del usuario telegramd).

Linux hace una correspondencia de usuarios dentro del contenedor, comenzando desde el 0, hacia identificadores (sub UIDs) en el host, comenzando por lo que tengas indicado en /etc/subuid. Veíamos que mi usuario daniel puede asignar comenzando desde el 100000 y hasta 65536 identificadores como máximo:

$ cat /etc/subuid
daniel:100000:65536
side:165536:65536

De ahí que para mapear el 500 de dentro del contenedor, el resultado en el host haya sido 100000 + 500 – 1 (porque comienza en 0) = 100499.

Finalmente (y como root) solo queda dejar listo el directorio para que Prosody pueda escribir en él, desde el host:

root$ chgrp prosody telegram2
root$ chmod g+w telegram2

Y otra vez como usuario normal:

$ ls -ald telegram2
drwxrwxr-x 2 100499 prosody 4096 oct  6 13:24 telegram2

Copiar desde Telegram

Con LXC nunca me llegó a funcionar bien el tema de montar volúmenes para compartir ficheros, de ahí que tuviera que montar una fiesta bastante engorrosa en la que las copias se hacía mediante ssh.

Es por ese motivo que os llamo la atención sobre el hecho de que el código que estoy desplegando ahora para el módulo que comunica Telegram con XMPP es la rama feature/inside_podman. Aquí podréis ver la simplificación de la función copy_to_prosody.

Copiar hace Telegram también se simplificará, pero eso lo tengo pendiente y va en el código del módulo de Prosody.

Punto de entrada

Finalmente, os quería comentar el cambio que supone el nuevo «punto de entrada» al contenedor. Ahí creo que es donde se ve más claro que un contenedor no es más que una especie de proceso aislado que lleva todo lo necesario para funcionar. Para propósitos de desarrollo, el punto de entrada por defecto siempre es un shell de Bash. Pero cuando vamos a poner el contenedor a funcionar, se va a invocar un script que está destinado a arrancar el proceso interesante. Ese script tiene el siguiente contenido:

#!/bin/bash
/home/telegramd/tg/bin/telegram-cli --daemonize \
				    --accept-any-tcp \
				    --permanent-msg-ids \
				    --permanent-peer-ids \
				    --enable-msg-id \
				    --phone=USUARIO \
				    --sync-from-start \
				    --wait-dialog-list \
				    --disable-readline \
				    --tcp-port=2392 \
				    --lua-script /home/telegramd/telegram2xmpp/tg2xmpp.lua

Como en los contenedores Podman/Docker no hay proceso de init, esa es la manera de comunicar qué proceso va a arrancar cuando arranque el contenedor, y con qué parámetros. Otra opción hubiera sido pasar directamente estos parámetros mediante buildah, pero creo que así queda más limpio el script de construcción de la imagen y la llamada a podman run es más sencilla.


Archivado en categoría(s) GNU/Linux

Enlace permanente



Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.