Skip to main content

Mejores prácticas para crear contenedores de aplicaciones Python con Docker

Escrito por

Daniel Campos Olivares

blog feature snyk python security

11 de noviembre de 2021

0 minutos de lectura

Al leer muchos blogs sobre contenedores Docker para Python, vimos que la mayoría de las publicaciones ofrecen ejemplos de cómo crear contenedores para una aplicación Python sin importar el framework (Django, Flask, Falcon, etc.). Por ejemplo, podrías encontrar algo como esto:

FROM python
WORKDIR /usr/app
COPY . .
RUN pip install -r requirements.txt
CMD [ "python", "app.py" ]

Con este Dockerfile, podemos crear y ejecutar una aplicación Python con Flask:

docker build -t flask-application .
docker run -p 8080:5000 flask-application

Dos pasos sencillos y funciona de maravilla, ¿verdad?

Aunque este ejemplo es sencillo y útil para demostraciones y tutoriales para comenzar, deja sin atender muchas inquietudes importantes. Por eso, en esta publicación abordaremos esas inquietudes y veremos seis mejores prácticas para crear contenedores de aplicaciones Python con Docker. Exploraremos por qué deberías:

  1. Usar etiquetas explícitas y deterministas para las imágenes base de Docker en aplicaciones Python con contenedores.

  2. Separar las dependencias del código fuente.

  3. Usar Python WSGI en producción.

  4. Ejecutar contenedores con los privilegios mínimos posibles (y nunca como root).

  5. Manejar los estados no saludables de tu aplicación.

  6. Encontrar y corregir vulnerabilidades de seguridad en la imagen Docker de tu aplicación Python.

1. Usa etiquetas explícitas y deterministas para las imágenes base de Docker en aplicaciones Python con contenedores

Aunque usar python como imagen base para nuestra aplicación Python en Docker parezca lógico, no queda claro qué versión de Python se está usando.

Al momento de escribir este artículo, la imagen base mencionada en el Dockerfile corresponde a una imagen base con Python 3.10. ¿Por qué? Como no agregamos una etiqueta específica, se usó de forma predeterminada la versión :latest de esa imagen base, que resulta ser 3.10, según la página oficial de imágenes en Docker Hub

Como queremos controlar las versiones de Python que usamos en nuestros contenedores, siempre debemos especificar esa información de versión en el Dockerfile.

Entonces, como quiero usar Python versión 3.10, solo tendría que agregar la etiqueta :3.10 a mi Dockerfile, ¿verdad?

Bueno... No exactamente.

La etiqueta de imagen base de Docker :3.10 corresponde a un sistema operativo completo con Python 3.10 instalado, que incluye una gran cantidad de bibliotecas que probablemente nunca usarás. Además, la gran cantidad de software tiene un efecto secundario: aumenta la superficie de ataque debido a las vulnerabilidades de seguridad presentes en esas bibliotecas.

Si introduces una imagen Docker de Python de gran tamaño, será más difícil mantenerla y mantener actualizadas todas esas versiones de bibliotecas.

Si usamos la herramienta Snyk Advisor para examinar una imagen base de Python, podemos ver que la imagen base de Docker python:3.10 tiene 12 problemas de gravedad alta, 27 de gravedad media y 132 de gravedad baja. Así que, de forma predeterminada, la imagen Docker de Python comienza con al menos 171 vulnerabilidades de seguridad, ¡y todavía no agregamos nada!

Además, como en la práctica estamos incorporando un sistema operativo completo, el tamaño de la imagen base será bastante grande para nuestro servidor de aplicaciones Python. Esto hará que las compilaciones sean más lentas y requerirá más espacio. Por lo general, hay pocas reglas para seleccionar una imagen base, pero dos son clave.

Mejores prácticas para elegir una imagen Docker de Python

  1. Elige la imagen base más pequeña que cumpla con todos tus requisitos y luego construye sobre ella. Las imágenes más pequeñas tienen menos vulnerabilidades, consumen menos recursos y contienen menos paquetes innecesarios..

  2. Usar la etiqueta con nombre no basta para garantizar que siempre usarás la misma imagen base. La única forma de garantizarlo es usar el digest de la imagen.

Con esa información, volvamos a Snyk Advisor y revisemos qué etiquetas alternativas recomienda. Esta herramienta ofrece una excelente vista general de las vulnerabilidades y el tamaño de las imágenes base, lo que nos ayudará mucho a tomar una decisión.

Como nos interesa usar Python 3.10 en nuestra aplicación, buscaremos esa etiqueta específica.

Página de Snyk Advisor para la imagen oficial de Python en Docker, con el comando docker pull y recomendaciones de etiquetas alternativas con datos de gravedad, actualización y tamaño

Además de las vulnerabilidades mencionadas, podemos ver que esta imagen base tiene un tamaño aproximado de 350 MB, está basada en Debian 11 y tiene 427 paquetes instalados. Consideramos que esta imagen es demasiado para nuestra pequeña aplicación Python.

Por otro lado, también está la imagen base de Docker para Python :3.10-slim, con 1 problema de gravedad alta, 1 de gravedad media y 35 de gravedad baja. La imagen base de Docker tiene un tamaño de 46.2 MB, usa un sistema operativo basado también en Debian 11 y tiene 106 paquetes instalados. Elegir esta imagen base de Docker para nuestro servidor de aplicaciones Python en lugar de la predeterminada reducirá la cantidad de vulnerabilidades de seguridad, el espacio en disco y el número de bibliotecas instaladas, además de satisfacer nuestras necesidades para la versión de la aplicación Python :3.10.

¡Y eso sería todo! Solo agrego la etiqueta :3.10-slim y ¡listo!

¡Casi! Cumplimos la primera regla: tenemos una imagen base de Docker pequeña que satisface nuestros requisitos. Pero todavía debemos resolver el segundo problema: garantizar que usemos exactamente esa imagen base de Docker cada vez que compilemos nuestro servidor de aplicaciones Python.

Para hacerlo, tenemos varias opciones:

  1. Obtener el digest de la imagen base de Docker desde Docker Hub.

  2. Descargar la imagen Docker en nuestra computadora con docker pull python:3.10-slim, lo que muestra el digest de la imagen Docker:

3.10-slim: Pulling from library/python
7d63c13d9b9b: Pull complete
6ad2a11ca37b: Pull complete
1d79bc863ed3: Pull complete
c72b5f03bec8: Pull complete
0c3b0c5ce69b: Pull complete
Digest: sha256:2bac43769ace90ebd3ad83e5392295e25dfc58e58543d3ab326c3330b505283d
Status: Downloaded newer image for python:3.10-slim
docker.io/library/python:3.10-slim

Si ya tenemos la imagen Docker de Python en nuestra computadora, podemos obtener el digest de la imagen existente en el disco con el comando docker images --digests | grep python:

python    3.10-slim    sha256:2bac43769ace90ebd3ad83e5392295e25dfc58e58543d3ab326c3330b505283d

Una vez que tenemos el digest de la imagen base, solo tenemos que agregarlo al Dockerfile mencionado:

Dockerfile

FROM python:3.10-slim@sha256:2bac43769ace90ebd3ad83e5392295e25dfc58e58543d3ab326c3330b505283d
WORKDIR /usr/app
COPY . .
RUN pip install -r requirements.txt
CMD [ "python", "app.py" ]

Esta práctica garantiza que cada vez que volvamos a crear la imagen Docker para esta aplicación Python, se usen las mismas versiones del sistema operativo subyacente y de las bibliotecas. Esto permite una compilación determinista.

2. Separa las dependencias del código fuente

Esta segunda mejor práctica evita uno de los errores más comunes en cualquier tipo de imagen Docker que incluya proyectos con dependencias. Primero, veamos la mala práctica:

  1. Copiar todo el contenido de la carpeta del proyecto al contexto de la imagen.

  2. Instalar las dependencias.

  3. Ejecutar la aplicación.

Bueno, funciona, pero hay mucho por mejorar. Para empezar, cuando desarrollas un proyecto localmente, solo instalas las dependencias cuando cambian, ¿verdad? Así que no hagas que tu imagen Docker las descargue e instale cada vez que cambies la menor línea de código.

Esta mejor práctica consiste en optimizar las capas de una imagen Docker. Si queremos aprovechar el sistema de caché durante una compilación de Docker, siempre debemos tener en cuenta una cosa al escribir un Dockerfile: las capas deben ordenarse según la probabilidad de que cambien.

Veamos el Dockerfile que hemos usado hasta ahora. Cada vez que compilamos esa imagen de aplicación Python, el software Docker examina las distintas capas y responde a esta pregunta: ¿Cambió algo o puedo usar lo que hice antes?

En la versión actual del contenido del Dockerfile, cualquier cambio en la carpeta del proyecto vuelve a activar la instrucción COPY y, después, el resto de las capas de compilación. Eso no tiene mucho sentido y deja bastante margen para optimizar y acelerar el proceso.

Mejoremos esto con el siguiente Dockerfile:

FROM python:3.10-slim@sha256:2bac43769ace90ebd3ad83e5392295e25dfc58e58543d3ab326c3330b505283d
WORKDIR /usr/app

COPY requirements.txt .
RUN pip install -r requirements.txt

COPY . .
CMD [ "python", "app.py" ]

Con este nuevo Dockerfile, la próxima vez que Docker compruebe si se pueden reutilizar las capas, si no encuentra cambios en el archivo requirements.txt, pasará directamente a la instrucción COPY, que se resolverá en cuestión de segundos. Con este pequeño cambio, aceleramos gran parte del proceso de compilación: ya no tendrás que esperar minutos entre compilaciones cada vez que modifiques algo en el código.

Es importante tener en cuenta que es posible que no todas tus dependencias estén empaquetadas como wheels. En ese caso, habría que instalar un compilador en la imagen.

¡Pero nos dijiste que usáramos la imagen más pequeña posible para ejecutar la aplicación!

Tienes razón, y por eso vamos a mostrarte otra increíble función de Docker: las compilaciones en varias etapas.

Compilaciones en varias etapas

La compilación en varias etapas consiste en usar una imagen Docker con más herramientas para compilar las dependencias necesarias y, después, copiar solo los artefactos necesarios a la imagen Docker de Python que realmente se usará.

Un ejemplo con aplicaciones basadas en Node.js sería el siguiente:

FROM node:latest AS build
ARG NPM_TOKEN
WORKDIR /usr/src/app
COPY package*.json /usr/src/app/
RUN npm install

FROM node:lts-alpine@sha256:b2da3316acdc2bec442190a1fe10dc094e7ba4121d029cb32075ff59bb27390a
WORKDIR /usr/src/app
COPY --from=build /usr/src/app/node_modules /usr/src/app/node_modules
COPY . .
CMD ["node", "server.js"]

Nota: Este es un breve ejemplo que solo demuestra las capacidades de la compilación en varias etapas. Si quieres aprender las mejores prácticas para crear contenedores de aplicaciones Node.js correctamente, consulta este artículo de Liran Tal (su nombre te suena, ¿no?) y Yoni Goldberg.

Las compilaciones en varias etapas para Node.js son bastante fáciles de manejar, ya que la carpeta node_modules se encuentra en la misma carpeta que el proyecto. Sin embargo, no ocurre lo mismo con las aplicaciones Python.

Si ejecutamos pip install, se instalarán muchas cosas en distintos lugares, lo que hará imposible realizar una compilación en varias etapas. Para resolverlo, tenemos dos opciones:

  1. Usar pip install --user

  2. Usar un virtualenv

Usar pip install --user podría parecer una buena opción, ya que todos los paquetes se instalarán en el directorio ~/.local y será bastante fácil copiarlos de una etapa a otra. Pero esto crea otro problema: agregarías a la imagen base final de Docker todas las dependencias a nivel de sistema de la imagen que usamos para compilar las dependencias, y no queremos que eso ocurra (recuerda nuestra mejor práctica para lograr una imagen base de Docker lo más pequeña posible).

Descartada esa primera opción, exploremos la segunda: usar un virtualenv. Si lo hacemos, el Dockerfile quedaría así.

FROM python:3.10-slim as build
RUN apt-get update
RUN apt-get install -y --no-install-recommends \
	      build-essential gcc 

WORKDIR /usr/app
RUN python -m venv /usr/app/venv
ENV PATH="/usr/app/venv/bin:$PATH"

COPY requirements.txt .
RUN pip install -r requirements.txt

FROM python:3.10-slim@sha256:2bac43769ace90ebd3ad83e5392295e25dfc58e58543d3ab326c3330b505283d
WORKDIR /usr/app/venv
COPY --from=build /usr/app/venv ./venv
COPY . .

ENV PATH="/usr/app/venv/bin:$PATH"
CMD [ "python", "app.py" ]

Ahora tenemos todas las dependencias necesarias, pero sin los paquetes adicionales que se requieren para compilarlas.

Problemas conocidos con las compilaciones en varias etapas para aplicaciones Python con contenedores

Actualmente, existe un problema conocido: las etapas previas no se guardan en caché. La forma más sencilla de resolverlo es usar BuildKit y agregar el argumento BUILDKIT_INLINE_CACHE=1 al proceso de compilación.

La primera vez compilaremos de la forma habitual, pero en las compilaciones posteriores usaremos el siguiente comando:

export DOCKER_BUILDKIT=1
docker build -t flask-application --cache-from flask-application --build-arg BUILDKIT_INLINE_CACHE=1 .

Necesitarás la variable de entorno DOCKER_BUILDKIT=1 para que Docker sepa que debe usar BuildKit durante el proceso de compilación. También puedes habilitarlo agregando la siguiente configuración al archivo de configuración del software Docker en /etc/docker/daemon.json (en ese caso, no necesitarías la variable de entorno):

{ "features": { "buildkit": true } }

3. Usa Python WSGI en producción

Dejar habilitado el modo de depuración en aplicaciones Python preparadas para producción es un gran error y una situación propicia para un incidente de seguridad. Por desgracia, es un error común que hemos visto en muchos artículos de blog sobre aplicaciones Flask de Python con contenedores y otros frameworks de aplicaciones Python WSGI.

El depurador permite ejecutar código Python arbitrario desde el navegador, lo que crea un riesgo de seguridad enorme. Se puede proteger hasta cierto punto, pero siempre será una vulnerabilidad. Lo repetimos por seguridad: ¡No ejecutes servidores de desarrollo ni depuradores en un entorno de producción! Esto también se aplica a tus aplicaciones Python con contenedores.

Entendemos que es más fácil implementar tus aplicaciones Python con Flask en modo de depuración que configurar un servidor WSGI y el servidor web o proxy, pero solo es más fácil hasta que tengas que explicar por qué hackearon tu aplicación.

¿Cómo se soluciona? Primero, debes decidir qué implementación de servidor WSGI quieres usar. Estos cuatro son los más comunes para aplicaciones Python:

  • Green Unicorn (Gunicorn): modelo de procesos de trabajo prefork adaptado del proyecto Unicorn de Ruby.

  • uWSGI: implementación de servidor WSGI versátil, de alto rendimiento y bajo consumo de recursos.

  • mod_wsgi — Un módulo de Apache que puede alojar cualquier aplicación web de Python compatible con la especificación WSGI de Python.

  • CherryPy — Un framework HTTP orientado a objetos y con un estilo propio de Python que también funciona como servidor WSGI.

En este artículo usaremos Gunicorn como ejemplo, pero puedes consultar la documentación y la información de todos ellos para elegir el que mejor se adapte a tus necesidades. En esta publicación no veremos la configuración, ya que depende por completo del caso de uso.

Para la parte de la contenerización, solo tendremos que:

  1. Incluir la dependencia gunicorn en requirements.txt

  2. Cambiar el punto de entrada del contenedor de la aplicación Python (consulta la instrucción CMD para hacer este cambio):

FROM python:3.10-slim as build
RUN apt-get update
RUN apt-get install -y --no-install-recommends \
	build-essential gcc 

WORKDIR /usr/app
RUN python -m venv /usr/app/venv
ENV PATH="/usr/app/venv/bin:$PATH"

COPY requirements.txt .
RUN pip install -r requirements.txt

FROM python:3.10-slim@sha256:2bac43769ace90ebd3ad83e5392295e25dfc58e58543d3ab326c3330b505283d
WORKDIR /usr/app
COPY --from=build /usr/app/venv ./venv
COPY . .

ENV PATH="/usr/app/venv/bin:$PATH"
CMD [ "gunicorn", "--bind", "0.0.0.0:5000", "manage:app" ]

Después de volver a crear la aplicación Python con este nuevo Dockerfile, podemos ejecutarla y comprobar que la aplicación Flask está lista para procesar solicitudes:

docker run -p 8080:5000 flask-application

Nota: Para implementaciones listas para producción, recomendamos no vincular directamente al host el puerto expuesto por Gunicorn. En su lugar, recomendamos implementar un servidor proxy inverso en la misma red para que gestione todas las solicitudes HTTP y sirva los archivos estáticos.

4. Ejecuta las aplicaciones Python en contenedores con los mínimos privilegios posibles (y nunca como root)

El principio de privilegio mínimo es un control de seguridad que existe desde los primeros días de Unix, y siempre debemos seguirlo al ejecutar nuestras aplicaciones Python en contenedores.

La imagen oficial de Docker python no incluye un usuario con privilegios de forma predeterminada, por lo que tendremos que crear uno para poder ejecutar el proceso con un usuario que tenga los mínimos privilegios.

Para ello, agregaremos los siguientes comandos groupadd al Dockerfile de la imagen final (la segunda etapa de la compilación de varias etapas), que es la que ejecuta el proceso gunicorn:

FROM python:3.10-slim@sha256:2bac43769ace90ebd3ad83e5392295e25dfc58e58543d3ab326c3330b505283d

RUN groupadd -g 999 python && \
    useradd -r -u 999 -g python python
USER 999
WORKDIR /usr/app

COPY --from=build /usr/app/venv ./venv
COPY . .

ENV PATH="/usr/app/venv/bin:$PATH"
CMD [ "gunicorn", "--bind", "0.0.0.0:5000", "manage:app" ]

Sin embargo, el problema es que, con las modificaciones anteriores, el usuario python es propietario del proceso del sistema que ejecuta Docker... ¿Y qué pasa con la propiedad de los archivos copiados o del directorio WORKDIR? De forma predeterminada, el compilador de Docker crea el directorio WORKDIR si no existe, pero lo crea con el usuario del sistema root como propietario. Por eso, cualquier operación que implique escribir en ese directorio podría provocar un error fatal en nuestra aplicación. Además, los archivos copiados pertenecerán a root de forma predeterminada si no cambiamos ese comportamiento, aunque ya hayamos cambiado de usuario.

Solucionémoslo:

FROM python:3.10-slim@sha256:2bac43769ace90ebd3ad83e5392295e25dfc58e58543d3ab326c3330b505283d

RUN groupadd -g 999 python && \
    useradd -r -u 999 -g python python

RUN mkdir /usr/app && chown python:python /usr/app
WORKDIR /usr/app

COPY --chown=python:python --from=build /usr/app/venv ./venv
COPY --chown=python:python . .

USER 999

ENV PATH="/usr/app/venv/bin:$PATH"
CMD [ "gunicorn", "--bind", "0.0.0.0:5000", "manage:app" ]

Con las instrucciones COPY actualizadas de arriba, evitamos comportamientos inesperados relacionados con la propiedad de los archivos del directorio WORKDIR y nos aseguramos de que todos los archivos pertenezcan al mismo usuario que ejecutará el proceso.

5. Gestiona los estados de inactividad de tu aplicación Python en contenedores

Al implementar una aplicación, debes tener en cuenta los posibles eventos o problemas no controlados que podrían hacer que la aplicación pase a un estado de inactividad en el que: 1) deje de funcionar, pero 2) no detenga el proceso. Si esto sucede, no se notificará al contenedor y tendrás un servidor de aplicaciones Python en ejecución que ya no responde a las solicitudes HTTP.

Para evitarlo, queremos implementar un endpoint de verificación de estado. Para comprobar el estado de tu aplicación Python en contenedores, siempre recomendamos incluir un endpoint HTTP de monitoring o de health para asegurarnos de que la aplicación siga procesando correctamente las solicitudes de los usuarios. Con algunos frameworks de aplicaciones web para Python, como Flask, esta tarea es sencilla (consulta el ejemplo de Flask a continuación). Además, al combinarlo con la instrucción HEALTHCHECK de Docker, te asegurarás de que se monitoree correctamente el estado de tu aplicación.

A continuación, se muestra un fragmento de una aplicación Flask de Python que agrega un endpoint HTTP /health:

@app.route('/health', methods=['GET'])
def health():
	# Handle here any business logic for ensuring you're application is healthy (DB connections, etc...)
    return "Healthy: OK"

Una vez que tengas ese endpoint en tu aplicación, solo tendrás que incluir la instrucción HEALTHCHECK en tu Dockerfile:

FROM python:3.10-slim@sha256:2bac43769ace90ebd3ad83e5392295e25dfc58e58543d3ab326c3330b505283d

RUN groupadd -g 999 python && \
    useradd -r -u 999 -g python python

RUN mkdir /usr/app && chown python:python /usr/app
WORKDIR /usr/app

COPY --chown=python:python --from=build /usr/app/venv ./venv
COPY --chown=python:python . .

USER 999

ENV PATH="/usr/app/venv/bin:$PATH"
CMD [ "gunicorn", "--bind", "0.0.0.0:5000", "manage:app" ]
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 CMD curl -f http://localhost:5000/health

Si implementas en Kubernetes, debes saber que se ignoran las directivas HEALTHCHECK de Dockerfile. Debes incluir en tu archivo YAML una sonda de actividad, preparación e inicio de Kubernetes equivalente.

Por lo tanto, para adaptar la instrucción HEALTHCHECK anterior a las implementaciones en Kubernetes:

...
   livenessProbe:
     httpGet:
       path: /health
       port: 5000
     initialDelaySeconds: 5
     periodSeconds: 30
     timeoutSeconds: 30
     failureThreshold: 3
...

Ten en cuenta que esta configuración debe ir anidada en la sección container spec del archivo YAML del pod o de la implementación. Ahora, con una política de reinicio always o unless_stopped, el contenedor de nuestra aplicación Python siempre se reiniciará si pasa a un estado de inactividad.

6. Detecta y corrige vulnerabilidades de seguridad en la imagen Docker de tu aplicación Python

Ya establecimos que las imágenes base de Docker más grandes plantean muchos desafíos, como una gran pila de software que debemos mantener y actualizar con las correcciones de seguridad, entre otros.

También vimos cómo usar Snyk Advisor para determinar el tamaño y las métricas de vulnerabilidades de las distintas imágenes base. Pero Advisor es solo la punta del iceberg. Snyk es una plataforma gratuita de seguridad para desarrolladores que puedes usar para probar desde tu propio código Python y sus dependencias (como las de requirements.txt) hasta la imagen de contenedor Python que ejecuta tu aplicación, e incluso las configuraciones de Terraform o Kubernetes que lo organizan todo.

Lo mejor de Snyk es que ofrece recomendaciones para corregir las vulnerabilidades que detecta. Así, en lugar de solo decirte qué vulnerabilidades de seguridad existen, crea automáticamente un pull request con una corrección o brinda recomendaciones para remediarlas. Y lo hace desde las herramientas (IDE, CLI, Docker, etc.) y los flujos de trabajo (Git, CI/CD, etc.) que ya usas.

Ejemplo: analizar nuestra aplicación Python en contenedores con Snyk

Veamos cómo funciona desde la CLI de Snyk cuando usamos la imagen Docker python:3.8 para compilar esta aplicación de ejemplo de Flask en Python.

Si instalas la CLI de Snyk, puedes usarla para analizar las dependencias de tu proyecto Python, tu código Python y mucho más. Primero, instalamos la CLI de Snyk. Si tienes un entorno Node.js, puedes hacerlo con el administrador de paquetes npm de la siguiente manera:

npm install -g snyk

Si usas macOS o Linux y tienes Homebrew, puedes instalarla así:

brew tap snyk/tap
brew install snyk

Para conocer otros métodos de instalación, consulta nuestra guía sobre cómo instalar la CLI de Snyk.

A continuación, debemos autenticarnos desde la CLI para obtener un token de API válido y consultar la base de datos de vulnerabilidades:

snyk auth

Una vez hecho lo anterior, podemos compilar una imagen Docker local de la aplicación Python con la imagen base python:3.8:

❯ docker build . -t python-flask-app
FROM python:3.8 as build
[+] Building 5.2s (8/13)
 => [internal] load build definition from Dockerfile
 => => transferring dockerfile: 513B
 => [internal] load .dockerignore
 => => transferring context: 2B
 => [internal] load metadata for docker.io/library/python:3.8
 => [auth] library/python:pull token for registry-1.docker.io
 => [internal] load build context
...

Ahora, analicémosla con Snyk ejecutando:

snyk container test python-flask-app

El resultado muestra lo siguiente (se acortó intencionalmente porque es muy extenso):

Testing python-flask-app...

✗ Low severity vulnerability found in tiff/libtiff5
  Description: Out-of-bounds Read
  Info: https://snyk.io/vuln/SNYK-DEBIAN11-TIFF-514595
  Introduced through: imagemagick@8:6.9.11.60+dfsg-1.3, imagemagick/libmagickcore-dev@8:6.9.11.60+dfsg-1.3
  From: imagemagick@8:6.9.11.60+dfsg-1.3 > imagemagick/imagemagick-6.q16@8:6.9.11.60+dfsg-1.3 > imagemagick/libmagickcore-6.q16-6@8:6.9.11.60+dfsg-1.3 > tiff/libtiff5@4.2.0-1
  From: imagemagick/libmagickcore-dev@8:6.9.11.60+dfsg-1.3 > imagemagick/libmagickcore-6.q16-dev@8:6.9.11.60+dfsg-1.3 > tiff/libtiff-dev@4.2.0-1 > tiff/libtiff5@4.2.0-1
  From: imagemagick/libmagickcore-dev@8:6.9.11.60+dfsg-1.3 > imagemagick/libmagickcore-6.q16-dev@8:6.9.11.60+dfsg-1.3 > tiff/libtiff-dev@4.2.0-1 > tiff/libtiffxx5@4.2.0-1 > tiff/libtiff5@4.2.0-1
  and 3 more...

✗ High severity vulnerability found in imagemagick/imagemagick-6-common
  Description: Information Exposure
  Info: https://snyk.io/vuln/SNYK-DEBIAN11-IMAGEMAGICK-1246513
  Introduced through: imagemagick/libmagickcore-dev@8:6.9.11.60+dfsg-1.3, imagemagick/libmagickwand-dev@8:6.9.11.60+dfsg-1.3, imagemagick@8:6.9.11.60+dfsg-1.3
  From: imagemagick/libmagickcore-dev@8:6.9.11.60+dfsg-1.3 > imagemagick/imagemagick-6-common@8:6.9.11.60+dfsg-1.3
  From: imagemagick/libmagickwand-dev@8:6.9.11.60+dfsg-1.3 > imagemagick/imagemagick-6-common@8:6.9.11.60+dfsg-1.3
  From: imagemagick@8:6.9.11.60+dfsg-1.3 > imagemagick/imagemagick-6.q16@8:6.9.11.60+dfsg-1.3 > imagemagick/libmagickcore-6.q16-6@8:6.9.11.60+dfsg-1.3 > imagemagick/imagemagick-6-common@8:6.9.11.60+dfsg-1.3
  and 24 more...

✗ Critical severity vulnerability found in python3.9/libpython3.9-stdlib
  Description: Improper Input Validation
  Info: https://snyk.io/vuln/SNYK-DEBIAN11-PYTHON39-1290158
  Introduced through: mercurial@5.6.1-4
  From: mercurial@5.6.1-4 > python3-defaults/python3@3.9.2-3 > python3-defaults/libpython3-stdlib@3.9.2-3 > python3.9/libpython3.9-stdlib@3.9.2-1
  From: mercurial@5.6.1-4 > python3-defaults/python3@3.9.2-3 > python3.9@3.9.2-1 > python3.9/libpython3.9-stdlib@3.9.2-1
  From: mercurial@5.6.1-4 > python3-defaults/python3@3.9.2-3 > python3-defaults/python3-minimal@3.9.2-3 > python3.9/python3.9-minimal@3.9.2-1
  and 4 more...

✗ Critical severity vulnerability found in glibc/libc-bin
  Description: Use After Free
  Info: https://snyk.io/vuln/SNYK-DEBIAN11-GLIBC-1296898
  Introduced through: glibc/libc-bin@2.31-13+deb11u2, meta-common-packages@meta
  From: glibc/libc-bin@2.31-13+deb11u2
  From: meta-common-packages@meta > glibc/libc-dev-bin@2.31-13+deb11u2
  From: meta-common-packages@meta > glibc/libc6@2.31-13+deb11u2
  and 1 more...

Organization:      snyk-demo-567
Package manager:   deb
Project name:      docker-image|python-flask-app
Docker image:      python-flask-app
Platform:          linux/amd64
Base image:        python:3.8.12-bullseye
Licenses:          enabled

Tested 427 dependencies for known issues, found 171 issues.

Base Image              Vulnerabilities  Severity
python:3.8.12-bullseye  171              6 critical, 6 high, 27 medium, 132 low

Recommendations for base image upgrade:

Alternative image types
Base Image                   Vulnerabilities  Severity
python:3.9-slim              37               1 critical, 0 high, 1 medium, 35 low
python:3.11-rc-slim          37               1 critical, 0 high, 1 medium, 35 low
python:3.8.12-slim-bullseye  37               1 critical, 0 high, 1 medium, 35 low
python:3.10-slim-buster      70               2 critical, 9 high, 9 medium, 50 low

La imagen incluye 427 dependencias en forma de bibliotecas de código abierto, como parte del contenido del sistema operativo Python 3.8. Estas introducen un total de 171 vulnerabilidades de seguridad en esta aplicación Flask de Python, debido a que elegimos la imagen base python:3.8.

En este punto, quizá te preguntes: «¿Cómo lo corrijo?». Por suerte, Snyk recomienda otras imágenes base a las que puedes actualizar o cambiar por completo para reducir la superficie de ataque.

Aquí puedes ver una captura clara de las recomendaciones para esta imagen base:

La salida de la terminal informa 171 vulnerabilidades en python:3.8.12-bullseye y compara el número de vulnerabilidades con el de imágenes base alternativas de Python.

Ahora podemos tomar una decisión fundamentada en datos para proteger nuestra aplicación Python. Si elegimos cualquiera de las imágenes Docker alternativas que recomienda Snyk, podemos reducir considerablemente la superficie de ataque del software incluido en nuestra aplicación.

Para tener aún más control sobre la seguridad de tu aplicación, conecta tus repositorios a la interfaz de Snyk para importar tu código fuente y Dockerfile. Así no solo podrás detectar estas vulnerabilidades, sino también monitorearlas de forma continua para encontrar nuevos problemas de seguridad. Aquí puedes ver el mismo informe sobre la imagen base de Docker en la interfaz de Snyk:

Detalles de la imagen de Snyk Container que muestran Python 3.8 en Debian 11 y recomendaciones para actualizar la imagen base, con el número de vulnerabilidades.

¿Qué es mejor que monitorear y encontrar vulnerabilidades de seguridad? ¡Corregirlas! :-)

Si conectas tus repositorios de Git a Snyk, también podemos crear pull requests en tu repositorio (¡automáticamente!) para ofrecerte estas actualizaciones de imágenes base de Docker, como puedes ver aquí:

pull request de GitHub que muestra una actualización de cart/Dockerfile, de node:10 a node:debian-buster-slim, para corregir vulnerabilidades

Si te interesa, consulta esta publicación de seguimiento sobre cómo automatizar la seguridad de contenedores con pull requests de Dockerfile.

¿Cómo se crean contenedores para las aplicaciones de Python?

Docker es una tecnología de virtualización de software que nos permite crear software reutilizable, multiplataforma y de implementación rápida en forma de aplicaciones de Python en contenedores, basadas en imágenes de Docker para Python. Estas aplicaciones se definen mediante infraestructura como código en un archivo llamado Dockerfile.

Para crear y usar una aplicación de Python en contenedores, ejecuta lo siguiente:

docker build -t flask-application .
docker run -p 8080:5000 flask-application

Recomendaciones de seguridad de Python para desarrolladores

Estas prácticas recomendadas deberían ayudarte a crear, administrar y proteger mejor tus aplicaciones Python en contenedores. Si te gustó leer sobre estas prácticas y te importa la seguridad de las aplicaciones y promover la seguridad en general, te recomendamos los siguientes recursos para seguir aprendiendo:

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.