Contenedores Docker con privilegios: ¿realmente los necesitas?
5 de noviembre de 2020
0 minutos de lecturaEsta semana, me fui por un agujero de conejo mientras hacía pruebas con Podman para entender por qué ejecutar cierto contenedor en una configuración sin privilegios de root requería la marca --privileged. Con toda razón, mi colega Eric Smalling preguntó por qué debía requerir esa marca.
En definitiva, --privileged es una forma abreviada de otorgar Todos Los Permisos, y aunque quizá pienses que no importa mucho cuando se ejecuta sin privilegios de root, sí rompe en cierta medida los principios de mínimo privilegio y confianza cero. Incluso cuando se ejecuta sin privilegios de root, siempre es recomendable entender exactamente qué necesita tu contenedor y otorgarle solo los permisos mínimos.
Aunque establecer esta marca en procesos que se ejecutan sin privilegios de root no les otorga realmente más privilegios que los que tiene el usuario, me dio curiosidad saber qué capacidades necesitaba realmente el proceso para que se recomendara esa configuración. Con el espíritu de exploración que nos caracteriza, me puse el equipo de espeleología y me adentré en el corazón de las tinieblas.
Vamos a explorarlo
Primero, veamos qué significa ejecutar contenedores sin privilegios de root. Todo se debe a la magia de los espacios de nombres de usuario en el kernel de Linux, que permiten a usuarios sin privilegios crear nuevos espacios de nombres de usuario. Cuando un usuario crea y entra en un nuevo espacio de nombres de usuario, se convierte en root dentro del contexto de ese espacio y obtiene la mayoría de los privilegios necesarios para iniciar un contenedor funcional. En los espacios de nombres de usuario, root obviamente no tiene los mismos privilegios que el root del sistema, pero esto significa que podemos ejecutar contenedores sin elevar los privilegios y, por lo tanto, reducir significativamente la superficie de ataque potencial de cualquier vulnerabilidad.
El sistema que uso es una máquina virtual con una instalación mínima de CentOS 8, que se ejecuta en VirtualBox en mi MacBook. Primero instalé podman con dnf:
Luego creé un directorio en el directorio de inicio de mi usuario y generé allí un certificado autofirmado:
Ahora quiero usar ese certificado para ejecutar un registro local de Docker con fines de prueba, y aquí es donde realmente comienza nuestro recorrido. Toda la documentación que encontré para hacerlo sugería usar la marca --privileged:
Como podemos ver en la cadena de comandos anterior, ejecutamos la imagen del registro con la etiqueta 2, creamos un montaje de volumen que vincula el directorio de certificados de mi directorio actual con /certs dentro del contenedor, pasamos algunas variables de entorno para configurar el registro y agregamos alegremente la marca --privileged, que indica a podman que ejecute este contenedor en modo privilegiado.
Para usar el registro con podman, luego debemos agregar una entrada en /etc/containers/registries.conf:
El modo privilegiado funciona bien, así que lo primero que quise ver fue qué pasaba si lo ejecutábamos sin privilegios. Fue bastante sencillo: el contenedor se inicia, luego falla y vuelve a iniciarse, y sus registros muestran que no puede acceder al certificado.
En este punto, supuse que se debía a las capacidades de Linux, ya que una de las principales funciones de la marca --privileged es permitir que el contenedor acceda a todas las capacidades que proporciona el kernel. Podemos comprobarlo con podman al ejecutar este contenedor en modo privilegiado:
Desde aquí fui a las páginas del manual de Linux, donde pude revisar en detalle qué permiten hacer los procesos con cada capacidad. También podemos usar podman para comprobar qué capacidades tiene nuestro contenedor en ejecución sin privilegios:
Al comparar esta lista con la página del manual de Linux, las capacidades efectivas en modo sin privilegios deberían ser suficientes para permitir que el contenedor lea archivos. De hecho, esta lista probablemente nos da más capacidades de las que necesita este contenedor en particular, pero volveremos a eso más adelante.
El siguiente paso es revisar el audit.log del host y, efectivamente:
Esta entrada nos indica que SELinux bloqueó la llamada de lectura del archivo domain.crt. En los sistemas CentOS y RHEL, SELinux está configurado en modo Enforcing de forma predeterminada, y podemos comprobarlo con la herramienta sestatus:
SELinux etiqueta archivos y directorios para administrar el acceso a ellos en modo Enforcing, y este etiquetado también se refleja en el comportamiento de los motores de contenedores. Estos inician procesos con la etiqueta container_t y el contenedor propiamente dicho se etiqueta como container_file_t. SELinux garantiza que los procesos solo puedan interactuar con archivos etiquetados de esta manera y, de forma predeterminada, deniega el acceso a los archivos que están fuera del contenedor. Cuando ejecutamos el contenedor con la marca --privileged, las etiquetas se desactivan y el contenedor se ejecuta con la etiqueta con la que se inició el motor de contenedores. Podemos verlo al revisar nuestros contenedores con podman. Este es un contenedor con privilegios:
Y este es uno que se ejecuta sin privilegios:
Al revisar el directorio de certificados en el sistema de archivos del host, vemos que tiene la etiqueta user_home_t:
En la práctica, esto significa que, en modo sin privilegios, el contenedor no puede acceder a esos archivos, aunque estén montados mediante bind mount en la imagen.
Entonces, en lugar de ejecutar nuestro contenedor con --privileged, tenemos un par de opciones para solucionar esto. Primero, podemos desactivar por completo las etiquetas con --security-opts label=disable en la línea de comandos de podman. Obviamente, esto no es lo ideal desde el punto de vista de la seguridad, así que tanto podman como Docker cuentan con un mecanismo para volver a etiquetar los montajes: de forma privada con la opción Z o, si el montaje se comparte, con la opción z.
Entonces, para solucionar el problema de nuestro contenedor, basta con ejecutarlo así:
Por último, volviendo a las capacidades, siempre es buena idea ejecutar los contenedores con la cantidad mínima posible de capacidades habilitadas. En mi configuración de prueba, es posible ejecutar este contenedor de registro sin ninguna capacidad:
Hay varias formas de determinar qué capacidades necesita tu contenedor. Por ejemplo, puedes revisar qué capacidades bloquea SELinux durante la operación en el registro de auditoría y luego agregar las necesarias con el argumento --cap-add. ¡La regla del mínimo privilegio siempre es la mejor opción! La moraleja de esta historia es que no hay que tirar al bebé junto con el agua de la bañera.
En resumen
Marcar los contenedores como --privileged, incluso en espacios de nombres de usuario, no es una buena práctica y rompe los principios de mínimo privilegio y confianza cero. Averigua qué necesita realmente tu contenedor antes de ejecutarlo: usa herramientas como SELinux para auditar qué capacidades y permisos solicita la imagen del contenedor, y configura solo esos permisos cuando ejecutes el contenedor en producción. A menudo verás que los requisitos son bastante mínimos.
Seguridad de contenedores centrada en los desarrolladores
Snyk encuentra y corrige automáticamente vulnerabilidades en imágenes de contenedores y cargas de trabajo de Kubernetes.


