Consejos y prácticas recomendadas para crear imágenes de contenedor seguras
6 de julio de 2021
0 minutos de lecturaCuando empiezas a analizar tus imágenes de contenedor, puede ser inquietante descubrir que tienen una gran cantidad de vulnerabilidades. A continuación, se muestra un análisis que hice la semana pasada de una imagen de Node vulnerable que creé. Aunque es un ejemplo bastante extremo, puedes ver que esta imagen, tal como viene, presenta más de 800 vulnerabilidades.

Ante esto, muchos nos quedamos paralizados, como un ciervo frente a los faros de un auto, cuando nos presentan una larga lista de CVE, sobre todo si nos enfocamos en el desarrollo de aplicaciones y no en la administración de sistemas. ¿Qué se supone que debo hacer con esta información? ¿Por dónde empiezo? Solo quería una imagen para ejecutar mi aplicación de Node y ya tengo ante mí esta enorme tarea de protegerla.
Ahora bien, lo más importante que debemos recordar es que corregir estos problemas en los contenedores no es igual que corregirlos en un sistema operativo. No vamos a actualizar paquetes individuales ni a administrar todo el sistema. Con los contenedores, debemos entender cómo llegaron las vulnerabilidades a nuestras imágenes antes de pensar en las estrategias que usaremos para corregirlas, como nuestra lista de 10 prácticas recomendadas de seguridad para Docker.
¿Qué contiene mi imagen de contenedor?
Lo primero que conviene entender en este contexto es cómo podrían estar construidas las imágenes que usamos. A menos que las creemos desde cero, es probable que hayamos partido de una imagen base en nuestro Dockerfile. Aunque la llamamos imagen base, es probable que también se haya creado a partir de una imagen principal, a la que se le instaló software durante el proceso de compilación. A su vez, esa imagen principal se creó de alguna manera, quizá a partir de otra imagen principal o mediante algún tipo de herramienta para crear sistemas de archivos raíz. Entender cómo llegó a nuestras imágenes el software que analizamos es fundamental para decidir qué estrategia seguir y reducir las vulnerabilidades.
Como ejemplo, veamos la imagen oficial de Nginx en Docker Hub. Si revisamos el Dockerfile de esta imagen, veremos que se basa en la imagen Debian Buster slim, a la que se le agregan software y configuración cuando se crea la imagen de Nginx.
A su vez, la imagen Debian Buster se crea a partir de otro Dockerfile, que toma una imagen scratch y le agrega un archivo tar.
Si investigamos cómo se crea ese archivo tar, veremos que es el resultado de la herramienta debuerreotype, una serie de scripts que el proyecto Debian usa para crear sistemas de archivos raíz. Así lo hace Debian, pero los demás sistemas operativos que suelen usarse como imágenes base se crean mediante distintos métodos.
La idea es que, incluso si solo analizamos nuestras imágenes base, la forma en que llega el software puede implicar un proceso largo y potencialmente complicado, difícil de seguir si no entiendes todos estos paradigmas.
Scratch
Algunas personas dirían que basta con usar scratch y crear nuestras propias imágenes desde cero, es decir, a partir de un sistema de archivos vacío. Este enfoque es válido en algunas circunstancias y puede funcionar bien con binarios de lenguajes compilados que no tienen dependencias, como Go o C. Sin embargo, con la mayoría de las demás opciones, terminarás manteniendo todo lo que se incluya en la imagen, lo que supone una carga considerable y continua. Si creamos muchas imágenes de contenedor distintas, esta carga puede volverse abrumadora rápidamente; además, podríamos perder la ventaja de tener una superficie de ataque más pequeña debido al esfuerzo de mantenimiento.
Confiar o no confiar
Al gestionar vulnerabilidades en imágenes, confiar en nuestra imagen base es una consideración clave. Siempre hay que encontrar un equilibrio entre confiar en la imagen upstream o decidir que debemos hacernos cargo de todo el proceso de compilación de nuestras imágenes y, por lo tanto, asumir la responsabilidad de todo el software que se instala en ellas.
Como vimos antes, también debemos confiar en toda la cadena de procesos de compilación que dio origen a la imagen que usamos, y puede ser difícil seguirla con claridad. Muchas imágenes de registros públicos están mal creadas o no reciben mantenimiento y, en general, estos registros no garantizan la calidad de la mayoría de las imágenes que alojan. Claro que esto no es distinto de cómo consumimos la mayor parte del software de código abierto, y muchos de los mismos factores de calidad que podrían influir en nuestras decisiones también se aplican aquí. ¿El software recibe mantenimiento y actualizaciones con regularidad? ¿Tiene una comunidad amplia de usuarios? ¿Hay empresas que lo respaldan? Puedes encontrar toda esta información en línea, así que tómate el tiempo para investigar qué estás usando en realidad. Snyk Advisor es una excelente herramienta para investigar.
Si decidimos confiar en nuestra imagen base upstream, cuando surjan problemas en ella, debemos buscar una solución upstream en lugar de terminar manteniendo nuestra propia versión derivada de la imagen base con paquetes actualizados. Por naturaleza, los contenedores están diseñados para ser inmutables. Si empezamos a actualizar paquetes durante el proceso de compilación del contenedor, estaremos desvirtuando el concepto de usar una imagen base. Esta estrategia se volverá inmanejable muy pronto, ya que nos convertiremos en los responsables de facto del mantenimiento de nuestra imagen.
Pero elegir una imagen base no siempre es tan sencillo como parece. Por ejemplo, la imagen base «oficial» de Python en Docker Hub tiene muchas vulnerabilidades y es muy grande. Esto es bastante común en las imágenes oficiales de entornos de ejecución, ya que, por diseño, deben ser genéricas para todos los casos de uso. Podríamos elegir la versión slim, que es más pequeña y tiene menos vulnerabilidades, o quizá buscar otra opción. Pero el repositorio tiene muchísimas etiquetas: ¿cómo elegimos?
Para empezar, probablemente no quieras usar en producción la etiqueta genérica latest de una imagen de framework de lenguaje. Es difícil saber qué versión del framework usa y podría cambiar en el futuro. Pero slim no es automáticamente la mejor opción: quizá tenga menos vulnerabilidades, pero entonces podrías tener que empezar a administrar las dependencias de compilación.
Y la opción ganadora es... las compilaciones de varias etapas
La práctica recomendada en este caso es usar compilaciones de varias etapas: usamos la imagen más grande y genérica para compilar el software y luego copiamos los artefactos de compilación a la versión slim para la implementación en producción. Así no tenemos que administrar las dependencias de compilación y podemos aprovechar el menor tamaño y la reducción de vulnerabilidades de la versión slim. También debemos usar versiones específicas del entorno de ejecución para saber exactamente qué entorno tendremos y asegurarnos de que no cambie sin que lo sepamos.
Prácticas recomendadas para elegir imágenes base
Estas son algunas recomendaciones generales para elegir imágenes base.
Confía en un proveedor upstream para que se encargue del trabajo pesado y corrija las vulnerabilidades por ti. Sus equipos son más grandes y, por eso, es mucho más probable que resuelvan los problemas rápidamente.
Fija tus aplicaciones a imágenes con versiones específicas: como mínimo, una versión principal, pero probablemente una secundaria. Así evitarás que las cosas cambien inesperadamente en el futuro.
Aprende a aprovechar las compilaciones de varias etapas. Esto te permite usar imágenes slim en la implementación y, al mismo tiempo, aprovechar combinaciones probadas durante la compilación.
Reconstruye con frecuencia. Muchas veces, así obtendrás correcciones de seguridad como parte del proceso de compilación.
Considera actualizar de vez en cuando. Las versiones nuevas también incluyen más correcciones de seguridad.
Analiza siempre tus imágenes para detectar vulnerabilidades
Al analizar tus imágenes y Dockerfiles con Snyk, puedes descubrir qué imágenes base alternativas puedes usar para reducir la cantidad total de vulnerabilidades. Snyk también puede crear automáticamente solicitudes de cambios (PR) en tus Dockerfiles para cambiar la imagen base. Y lo mejor de todo: puedes usarlo gratis.

En la parte 2 de esta serie del blog, veremos el software que agregamos a nuestras imágenes base y cómo podemos empezar a corregir las vulnerabilidades que contiene. Vuelve pronto o sigue a @snyksec en Twitter para recibir una notificación cuando se publique.
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.
