Seguridad en Docker Engine
Hay cuatro áreas principales a considerar al revisar la seguridad de Docker:
- La seguridad intrínseca del kernel y su soporte para espacios de nombres (namespaces) y grupos de control (cgroups).
- La superficie de ataque del propio demonio de Docker.
- Brechas en el perfil de configuración del contenedor, ya sea por defecto o cuando son personalizadas por los usuarios.
- Las características de seguridad de "endurecimiento" (hardening) del kernel y cómo interactúan con los contenedores.
Espacios de nombres del kernel (Kernel namespaces)
Los contenedores de Docker son muy similares a los contenedores LXC, y tienen características de seguridad similares. Cuando inicias un contenedor con docker run, detrás de escena Docker crea un conjunto de espacios de nombres y grupos de control para el contenedor.
Los espacios de nombres proporcionan la primera y más sencilla forma de aislamiento. Los procesos que se ejecutan dentro de un contenedor no pueden ver, y mucho menos afectar, los procesos que se ejecutan en otro contenedor o en el sistema host.
Cada contenedor también obtiene su propia pila de red, lo que significa que un contenedor no obtiene acceso privilegiado a los sockets o interfaces de otro contenedor. Por supuesto, si el sistema host está configurado de manera correspondiente, los contenedores pueden interactuar entre sí a través de sus respectivas interfaces de red, al igual que pueden interactuar con hosts externos. Cuando especificas puertos públicos para tus contenedores o utilizas enlaces (links), entonces se permite el tráfico IP entre contenedores. Pueden hacerse ping entre sí, enviar/recibir paquetes UDP y establecer conexiones TCP, pero eso se puede restringir si es necesario. Desde el punto de vista de la arquitectura de red, todos los contenedores en un host de Docker determinado están ubicados en interfaces de puente (bridge). Esto significa que son como máquinas físicas conectadas a través de un switch Ethernet común; ni más ni menos.
¿Qué tan maduro es el código que proporciona los espacios de nombres del kernel y la red privada? Los espacios de nombres del kernel se introdujeron entre la versión del kernel 2.6.15 y la 2.6.26. Esto significa que desde julio de 2008 (fecha del lanzamiento de la versión 2.6.26), el código de los espacios de nombres se ha ejercitado y analizado en una gran cantidad de sistemas de producción. Y hay más: el diseño y la inspiración para el código de los espacios de nombres son aún más antiguos. Los espacios de nombres son en realidad un esfuerzo para volver a implementar las características de OpenVZ de tal manera que pudieran fusionarse dentro del kernel principal. Y OpenVZ se lanzó inicialmente en 2005, por lo que tanto el diseño como la implementación son bastante maduros.
Grupos de control (Control groups)
Los Grupos de Control (cgroups) son otro componente clave de los contenedores de Linux. Implementan la contabilidad y limitación de recursos. Proporcionan muchas métricas útiles, pero también ayudan a garantizar que cada contenedor obtenga su parte justa de memoria, CPU y E/S de disco; y, lo que es más importante, que un solo contenedor no pueda tumbar el sistema al agotar uno de esos recursos.
Por lo tanto, aunque no juegan un papel en evitar que un contenedor acceda o afecte los datos y procesos de otro contenedor, son esenciales para defenderse de algunos ataques de denegación de servicio. Son particularmente importantes en plataformas multi-inquilino (multi-tenant), como las PaaS públicas y privadas, para garantizar un tiempo de actividad (y rendimiento) constante incluso cuando algunas aplicaciones comienzan a comportarse mal.
Los Grupos de Control también han estado presentes durante un tiempo: el código se inició en 2006 y se fusionó inicialmente en el kernel 2.6.24.
Superficie de ataque del demonio de Docker
Ejecutar contenedores (y aplicaciones) con Docker implica ejecutar el demonio de Docker. Este demonio requiere privilegios de root a menos que optes por el modo Rootless, por lo que debes tener en cuenta algunos detalles importantes.
En primer lugar, solo los usuarios de confianza deben tener permitido controlar tu demonio de Docker. Esta es una consecuencia directa de algunas potentes características de Docker. Específicamente, Docker te permite compartir un directorio entre el host de Docker y un contenedor invitado; y te permite hacerlo sin limitar los derechos de acceso del contenedor. Esto significa que puedes iniciar un contenedor donde el directorio /host sea el directorio / en tu host; y el contenedor puede alterar el sistema de archivos de tu host sin ninguna restricción. Esto es similar a cómo los sistemas de virtualización permiten compartir recursos del sistema de archivos. Nada te impide compartir tu sistema de archivos raíz (o incluso tu dispositivo de bloque raíz) con una máquina virtual.
Esto tiene una fuerte implicación de seguridad: por ejemplo, si instrumentas Docker desde un servidor web para aprovisionar contenedores a través de una API, debes ser aún más cuidadoso de lo habitual con la verificación de parámetros, para asegurarte de que un usuario malintencionado no pueda pasar parámetros manipulados que hagan que Docker cree contenedores arbitrarios.
Por esta razón, el endpoint de la API REST (utilizado por la CLI de Docker para comunicarse con el demonio de Docker) cambió en Docker 0.5.2, y ahora utiliza un socket Unix en lugar de un socket TCP vinculado a 127.0.0.1 (este último propenso a ataques de falsificación de solicitudes en sitios cruzados si da la casualidad de que ejecutas Docker directamente en tu máquina local, fuera de una VM). Luego puedes usar las comprobaciones de permisos Unix tradicionales para limitar el acceso al socket de control.
También puedes exponer la API REST a través de HTTP si decides hacerlo explícitamente. Sin embargo, si haces eso, ten en cuenta las implicaciones de seguridad mencionadas anteriormente. Ten en cuenta que incluso si tienes un firewall para limitar los accesos al endpoint de la API REST desde otros hosts en la red, el endpoint aún puede ser accesible desde los contenedores, y puede resultar fácilmente en una escalada de privilegios. Por lo tanto, es obligatorio proteger los endpoints de la API con HTTPS y certificados. No se permite exponer la API del demonio a través de HTTP sin TLS, y tal configuración hace que el demonio falle tempranamente al iniciarse, consulta Conexiones TCP no autenticadas. También se recomienda asegurarse de que sea accesible solo desde una red de confianza o VPN.
También puedes usar DOCKER_HOST=ssh://USER@HOST o ssh -L /path/to/docker.sock:/var/run/docker.sock en su lugar si prefieres SSH sobre TLS.
El demonio también es potencialmente vulnerable a otras entradas, como la carga de imágenes desde el disco con docker load, o desde la red con docker pull. A partir de Docker 1.3.2, las imágenes ahora se extraen en un subproceso en un chroot en plataformas Linux/Unix, siendo el primer paso en un esfuerzo más amplio hacia la separación de privilegios. A partir de Docker 1.10.0, todas las imágenes se almacenan y se accede a ellas mediante las sumas de comprobación criptográficas de sus contenidos, lo que limita la posibilidad de que un atacante cause una colisión con una imagen existente.
Finalmente, si ejecutas Docker en un servidor, se recomienda ejecutar exclusivamente Docker en el servidor y mover todos los demás servicios dentro de contenedores controlados por Docker. Por supuesto, está bien conservar tus herramientas de administración favoritas (probablemente al menos un servidor SSH), así como los procesos de monitoreo/supervisión existentes, como NRPE y collectd.
Capacidades del kernel de Linux (Linux kernel capabilities)
Por defecto, Docker inicia los contenedores con un conjunto restringido de capacidades (capabilities). ¿Qué significa eso?
Las capacidades convierten la dicotomía binaria "root/no-root" en un sistema de control de acceso detallado. Los procesos (como los servidores web) que solo necesitan vincularse a un puerto inferior al 1024 no necesitan ejecutarse como root: simplemente se les puede otorgar la capacidad net_bind_service en su lugar. Y existen muchas otras capacidades para casi todas las áreas específicas donde normalmente se necesitan privilegios de root. Esto significa mucho para la seguridad de los contenedores.
Los servidores típicos ejecutan varios procesos como root, incluido el demonio SSH, el demonio cron, los demonios de registro, los módulos del kernel, las herramientas de configuración de red y más. Un contenedor es diferente, porque casi todas esas tareas son manejadas por la infraestructura alrededor del contenedor:
- El acceso SSH normalmente es gestionado por un único servidor que se ejecuta en el host de Docker.
cron, cuando sea necesario, debe ejecutarse como un proceso de usuario, dedicado y adaptado para la aplicación que necesita su servicio de programación, en lugar de como una facilidad para toda la plataforma.- La gestión de registros también se entrega normalmente a Docker o a servicios de terceros como Loggly o Splunk.
- La gestión de hardware es irrelevante, lo que significa que nunca necesitas ejecutar
udevdo demonios equivalentes dentro de los contenedores. - La gestión de red ocurre fuera de los contenedores, imponiendo la separación de conceptos tanto como sea posible, lo que significa que un contenedor nunca debería necesitar ejecutar comandos como
ifconfig,routeoip(excepto cuando un contenedor está diseñado específicamente para comportarse como un enrutador o firewall, por supuesto).
Esto significa que, en la mayoría de los casos, los contenedores no necesitan en absoluto privilegios de root "reales". Y, por lo tanto, los contenedores pueden ejecutarse con un conjunto de capacidades reducido; lo que significa que "root" dentro de un contenedor tiene muchos menos privilegios que el "root" real. Por ejemplo, es posible:
- Denegar todas las operaciones de montaje ("mount").
- Denegar el acceso a sockets puros (raw sockets) (para evitar la suplantación de paquetes).
- Denegar el acceso a algunas operaciones del sistema de archivos, como la creación de nuevos nodos de dispositivo, cambiar el propietario de los archivos o alterar atributos (incluido el atributo de inmutabilidad).
- Denegar la carga de módulos.
Esto significa que incluso si un intruso logra escalar a root dentro de un contenedor, es mucho más difícil causar daños graves o escalar al host.
Esto no afecta a las aplicaciones web normales, pero reduce considerablemente los vectores de ataque por parte de usuarios malintencionados. Por defecto, Docker elimina todas las capacidades excepto aquellas necesarias, un enfoque de lista de permitidos en lugar de una lista de denegados. Puedes ver una lista completa de las capacidades disponibles en las páginas de manual de Linux.
Un riesgo principal al ejecutar contenedores Docker es que el conjunto predeterminado de capacidades y montajes otorgados a un contenedor puede proporcionar un aislamiento incompleto, ya sea de forma independiente o cuando se utiliza en combinación con vulnerabilidades del kernel.
Docker admite la adición y eliminación de capacidades, permitiendo el uso de un perfil no predeterminado. Esto puede hacer que Docker sea más seguro mediante la eliminación de capacidades, o menos seguro mediante la adición de las mismas. La mejor práctica para los usuarios sería eliminar todas las capacidades excepto aquellas explícitamente requeridas para sus procesos.
Verificación de firma de Docker Content Trust
Docker Engine se puede configurar para ejecutar únicamente imágenes firmadas. La función de verificación de firma de Docker Content Trust está integrada directamente en el binario dockerd. Esto se configura en el archivo de configuración de Dockerd.
Para habilitar esta función, se puede configurar trustpinning en daemon.json, mediante el cual solo se pueden descargar (pull) y ejecutar repositorios firmados con una clave raíz especificada por el usuario.
Esta función proporciona más información a los administradores de la que estaba disponible anteriormente con la CLI para imponer y realizar la verificación de firma de imágenes.
Para obtener más información sobre la configuración de la verificación de firma de Docker Content Trust, dirígete a Content trust en Docker (Confianza de contenido en Docker).
Otras características de seguridad del kernel
Las capacidades son solo una de las muchas características de seguridad proporcionadas por los kernels de Linux modernos. También es posible aprovechar sistemas existentes y conocidos como TOMOYO, AppArmor, SELinux, GRSEC, etc., con Docker.
Aunque actualmente Docker solo habilita las capacidades, no interfiere con los otros sistemas. Esto significa que hay muchas formas diferentes de endurecer un host de Docker. Aquí tienes algunos ejemplos:
- Puedes ejecutar un kernel con GRSEC y PAX. Esto añade muchas comprobaciones de seguridad, tanto en tiempo de compilación como de ejecución; también frustra muchos exploits, gracias a técnicas como la aleatorización de direcciones. No requiere una configuración específica de Docker, ya que esas características de seguridad se aplican a todo el sistema, independientemente de los contenedores.
- Si tu distribución viene con plantillas de modelos de seguridad para contenedores Docker, puedes usarlas directamente. Por ejemplo, distribuimos una plantilla que funciona con AppArmor y Red Hat viene con políticas de SELinux para Docker. Estas plantillas proporcionan una red de seguridad adicional (aunque se superpone en gran medida con las capacidades).
- Puedes definir tus propias políticas utilizando tu mecanismo de control de acceso favorito.
Al igual que puedes usar herramientas de terceros para aumentar los contenedores Docker, incluyendo topologías de red especiales o sistemas de archivos compartidos, existen herramientas para endurecer los contenedores Docker sin necesidad de modificar el propio Docker.
A partir de Docker 1.10, los espacios de nombres de usuario (User Namespaces) son compatibles directamente con el demonio de docker. Esta característica permite que el usuario root en un contenedor se mapee a un usuario que no sea uid-0 fuera del contenedor, lo que puede ayudar a mitigar los riesgos de escape del contenedor. Esta facilidad está disponible pero no habilitada por defecto.
Consulta el comando del demonio en la referencia de la línea de comandos para obtener más información sobre esta característica. Se puede encontrar información adicional sobre la implementación de espacios de nombres de usuario en Docker en esta publicación de blog.
Conclusiones
Los contenedores de Docker son, por defecto, bastante seguros; especialmente si ejecutas tus procesos como usuarios no privilegiados dentro del contenedor.
Puedes añadir una capa adicional de seguridad habilitando AppArmor, SELinux, GRSEC u otro sistema de endurecimiento adecuado.
Si piensas en formas de hacer que docker sea más seguro, agradecemos las solicitudes de características, pull requests o comentarios en los foros de la comunidad de Docker.