Pero... ¿qué pasa con los usuarios dentro del contenedor? Cuando no se define un usuario de forma específica en el Dockerfile el usuario que utiliza el contenedor es root. Pero a diferencia del sistema de ficheros, no se trata de un 'root especial para el contenedor' que solo tiene permisos de superadministrador dentro del contenedor y no fuera de él. Se trata del mismo root que el sistema anfitrión.
Esto en principio, como el contenedor es un sistema seguro del que un usuario que obtuviera acceso al mismo no puede salir, no pasa nada. Pero esto sabemos que no es así. Hay multitud de situaciones, vulnerabilidades y formas de escapar de un contenedor y conseguir acceso al sistema anfitrión. Y eso significa que si consiguen acceso a un contenedor inseguro con permisos de root y salen fuera de él, lo harán con permisos de root.
Una vez metido el miedo en el cuerpo, vamos a probarlo. Para ello he creado un repositorio en Github con algunos Dockerfiles que iré usando para probar qué ocurre con diferentes configuraciones. Para clonarlo en vuestra máquina local:
git clone https://github.com/daviddetorres/definiendo-usuarios-docker
En el repositorio hay 3 Dockerfiles, de los cuales, nos vamos a centrar primero en los que se llaman Dockerfile_usuariopordefecto y Dockerfile_conusuario. El primero no define ningún usuario, por lo que el contenedor al iniciarse lo hará con permisos de root. En el segundo he añadido la creación de un usuario dentro del propio Dockerfile. Las líneas que hacen añaden al usuario son las siguientes:
RUN addgroup -S usuariocontenedor && \
adduser -S -s /bin/false -G usuariocontenedor usuariocontenedor && \
chown -R usuariocontenedor:usuariocontenedor /app
USER usuariocontenedor
En la primera orden con RUN lo que hacemos es:
- Crear un grupo de sistema "usuariocontenedor"
- Crear un usuario de sistema "usuariocontenedor" sin shell y en ese grupo
- Cambiar el propietario de la carpeta donde hemos copiado la aplicación a ese usuario
Finalmente, con la línea "USER usuariocontenedor" lo que hacemos es indicarle a Docker que cuando se ejecute el contenedor, lo haga con ese usuario.
Ahora vamos a probarlo. En el repositorio están los comandos para crear dos carpetas en /tmp con un fichero cada una. Una de ellas es propiedad de root son permisos sólo para él. La otra pertenece a un usuario normal. Vamos a ver si desde los contenedores podemos leer estas carpetas montando la carpeta padre de ellas como volumen.
Cuando iniciamos los dos contenedores y desde el que no tiene definido usuario (y está utilizando el usuario root) ejecutamos la lectura del fichero del superadministrador y ¡SOPRESA! podemos leerlo. Sin embargo, como era de esperar, desde el contenedor en el que hemos definido el usuario no podemos leer ni uno ni otro (ya que son propiedad de otro usuario).
Pero no nos vamos a quedar ahí. ¿Qué usuario está usando el contenedor que está creando un usuario nuevo? Vamos a verlo viendo el usuario que ha creado el proceso con un ps.
¿Quién es ese usuario systemd+ que está ejecutando el contenedor? ¿No se supone que he creado un usuario que se llamaba usuariocontenedor? Pues resulta que el uid del usuario que ha creado el contenedor es el 100, porque en el contenedor es el primero que está libre. Sin embargo, en mi máquina donde estoy ejecutando el contenedor, el usuario con uid 100 ya existe... Se trata de un usuario de sistema llamado systemd-timesync (más información sobre este usuario, que sirve para el servicio de sincronización horaria, aquí).
Entonces... Tengo un usuario que he creado en el contenedor con la intención de que no tenga permisos, pero ¿y si coincide con un usuario en la máquina anfitrión que sí que tiene permisos por estar en grupos, sudo, etc.? La alternativa para hacerlo bien es indicar a la hora de crear el usuario un uid específico, y normalmente lo suficientemente alto como para poder asegurar que ese uid no pertenece a usuarios creados en la máquina anfitrión.
Para poder hacer eso, hay que instalar en nuestra imagen basada en Alpine un paquete especial llamado shadow que permite la creación de usuarios con uids altos. El Dockerfile donde se hace esto es el llamado Dockerfile_conusuariouid y la línea que crea el usuario se convierte en los siguiente:
RUN apk add shadow && \
/usr/sbin/groupadd -g 10001 --system usuariocontenedor && \
/usr/sbin/useradd --system -u 10001 -s /bin/false -g usuariocontenedor usuariocontenedor && \
chown -R usuariocontenedor:usuariocontenedor /app
Ahora, al hacer un ps del proceso desde la máquina anfitrión, vemos lo siguiente:
Ahora el usuario al que pertenece el proceso no lo identifica la máquina anfitrión como un usuario determinado (si fuera así nos indicaría el nombre), sino que aparece únicamente con un uid (10001) que es el que hemos definido en la creación de la imagen.
Como conclusiones, podemos decir que dejar las imágenes con el usuario por defecto si no es estrictamente necesario es una mala idea, ya que estamos dando un acceso de root a todo aquel que se haga con el poder del contenedor gracias a una vulnerabilidad (del código, de las librerías que usamos, de la imagen base, etc.). Esto es especialmente crítico cuando montamos volúmenes.
Otro de los efectos que hemos visto en este ejemplo es que un contenedor puede crear un usuario con un uid que la máquina anfitrión identifique como otro usuario ya creado anteriormente. Si el usuario con el que coincide el del contenedor tiene otros permisos con los que no hemos contado, tenemos también un posible agujero y escalada de privilegios inesperada. Por ellos es recomendable crear siempre los usuarios de los contenedores uids que podamos asegurar que no corresponden con usuarios de sistema o especiales en la máquina anfitrión.
No hay comentarios:
Publicar un comentario