Skip to main content

Docker para desarrolladores de Java: 5 cosas que debes saber para no poner en riesgo tu seguridad

Escrito por
docker java feature

20 de noviembre de 2020

0 minutos de lectura

Docker es la forma más utilizada de contenerizar aplicaciones. Con Docker Hub, es fácil crear y descargar imágenes preconfiguradas. Esto es muy práctico, ya que puedes usar estas imágenes de Docker Hub para crear rápidamente una imagen para tu aplicación Java. Sin embargo, la forma más ingenua de crear imágenes Docker personalizadas para tu aplicación Java conlleva muchos riesgos de seguridad. Entonces, ¿cómo hacemos que la seguridad sea una parte esencial de Docker para desarrolladores de Java?

Antes de entrar en cómo crear una excelente imagen Docker para tu aplicación Java, repasemos algunas preguntas frecuentes sobre el tema.

¿Cómo puedo dockerizar aplicaciones Java?

Puedes ejecutar tu aplicación Java en un contenedor Docker con solo copiar el archivo .jar o .war directamente en una imagen base de JRE, pero hay algunas cosas que debes tener en cuenta. Elegir los argumentos JVM adecuados y configurar el entorno de ejecución del contenedor es solo la mitad del trabajo. La elección de la imagen base es fundamental desde el punto de vista de la seguridad, porque si eliges mal podrías introducir vulnerabilidades.

Este artículo te dará información para comprender mejor el impacto de elegir una imagen base y te ayudará a encontrar la imagen más segura disponible para tu aplicación.

¿Cómo ayuda Docker a los desarrolladores de Java?

Al empaquetar tu aplicación Java en un contenedor, puedes definir la aplicación completa —incluidos el JRE, los ajustes de configuración, las dependencias del sistema operativo y los artefactos de compilación— en artefactos autocontenidos y desplegables llamados imágenes de contenedor. Estas imágenes se definen mediante software, lo que permite crearlas de forma totalmente repetible y ofrece a los desarrolladores una manera de ejecutar la misma plataforma en todos los entornos. Por último, los contenedores permiten experimentar más fácilmente con nuevas versiones de la plataforma u otros cambios directamente en sus equipos, sin necesidad de permisos especiales.

Elige la imagen base de Docker adecuada para tu aplicación Java

Al crear una imagen Docker, la basamos en una imagen que descargamos de Docker Hub. A esto lo llamamos imagen base. La imagen base es el fundamento de la nueva imagen que vas a crear para tu aplicación Java. La imagen base que elijas es esencial porque te permite aprovechar todo lo que incluye. Sin embargo, esto tiene un costo: si una imagen base tiene una vulnerabilidad, esta también estará presente en la imagen que crees.

Al analizar las imágenes base, vemos que muchas de las vulnerabilidades pertenecen a la capa del sistema operativo (SO) que usan. En nuestra investigación anterior de 2019, Desplazar la seguridad de Docker a la izquierda, ya mostramos que las vulnerabilidades que introduce la capa del SO pueden variar considerablemente según la distribución que elijas.

Gráfico de barras titulado «Vulnerabilidades en imágenes de sistemas operativos» que muestra: Debian 55, Debian stretch-slim 42, Ubuntu 31, CentOS 1, Fedora 0 y Alpine 0.

Informe de 2019: Desplazar la seguridad de Docker a la izquierda

Veamos un conjunto popular de imágenes base Docker para Java de Adoptopenjdk: openjdk11. Con la etiqueta predeterminada, esta imagen se basa en una distribución de Ubuntu. Sin embargo, también podemos elegir etiquetas para versiones específicas basadas, por ejemplo, en Debian, CentOS o Alpine (ten en cuenta que Alpine no se basa en glibc y puede no ser compatible con aplicaciones que hacen llamadas JNI nativas).

Gráfico de barras de vulnerabilidades por etiqueta: Debian 75, Centos 27, Latest (Ubuntu) 25 y Alpine 0.

Podemos concluir que elegir la imagen base adecuada es fundamental desde el punto de vista de la seguridad. Probablemente no necesites todos los binarios que incluye un sistema operativo completo. Es preferible crear la nueva imagen Docker para tu aplicación Java a partir de una imagen base mínima. Los binarios que no tienes no pueden causarte daño.

Además de mejorar la seguridad, una imagen base mínima reduce el tamaño de la imagen que crees. Una imagen Docker más pequeña también ocupa menos espacio y, muy probablemente, inicia más rápido. Otra opción es compilar con jib, que crea una imagen Java mínima sin necesidad de un Dockerfile.

Usa un JRE, no un JDK

Al crear una imagen Docker, debemos asignarle solo los recursos necesarios para funcionar correctamente. Esto significa que debemos empezar con un entorno de ejecución de Java (JRE) adecuado para la imagen de producción, no con el kit de desarrollo de Java completo (JDK). Además, la imagen de producción no debe incluir un sistema de compilación como Maven o Gradle. El producto de la compilación, por ejemplo, tu archivo jar, debería ser suficiente.

Aunque quieras compilar tu aplicación dentro de un contenedor Docker, puedes separar fácilmente la imagen de compilación de la imagen de producción mediante una compilación de varias etapas.

Por ejemplo: Quiero crear una imagen Docker para mi aplicación java-code-workshop. Es una aplicación basada en Spring Boot, compilada con Maven, que requiere Java 8.

La forma ingenua de crear esta imagen Docker para Java sería algo así:

Fragmento de código que muestra un Dockerfile que compila un proyecto Maven de Spring Boot con OpenJDK 8.

Elegí una imagen base que incluye Maven y OpenJDK 8, copio el código fuente en la imagen y ejecuto Maven para compilar y correr mi aplicación. Este ejemplo funciona perfectamente. Mi aplicación se inicia y funciona sin problemas. Sin embargo, la imagen Docker que acabo de crear ocupa 631 MB.

Cambiemos este Dockerfile y usemos una compilación de varias etapas:

Fragmento de código con estilo de terminal que muestra un Dockerfile de varias etapas para compilar y ejecutar una aplicación Java con Maven y OpenJDK.

Ahora sigo usando la imagen maven-openjdk8 para compilar mi proyecto. Sin embargo, esta no será la imagen resultante. Creo una nueva imagen basada en una imagen JRE de Java 8 mucho más pequeña y copio solo el jar ejecutable de Spring Boot. Ahora solo tengo que ejecutar el jar-file y ¡listo! El resultado es una imagen Docker que no incluye el JDK ni Maven, sino solo el JRE. El tamaño de la imagen se reduce drásticamente a 132 MB.

Las imágenes más pequeñas no solo son más fáciles de cargar y permiten ahorrar tiempo de inicio, sino que también son mucho más seguras. ¿Te imaginas qué podría pasar si, por alguna razón, un atacante accediera a un contenedor en ejecución que tuviera disponible el JDK, tu código fuente y una herramienta de compilación?

También puedes usar este método cuando necesites incluir secretos para acceder a un repositorio privado. No querrás que esos secretos queden en la caché de tu imagen de producción. Como no usarás la imagen de compilación en producción, es perfectamente aceptable usar ahí esos secretos. Con esta técnica, puedes seleccionar lo que necesitas de otras imágenes y crear una imagen Docker de producción con solo los recursos necesarios.

No ejecutes tu contenedor Docker como root

Al crear un contenedor Docker, de forma predeterminada se ejecutará como root. Aunque esto es práctico durante el desarrollo, no querrás hacerlo con tus imágenes de producción. Supongamos que, por cualquier motivo, un atacante accede a una terminal o puede ejecutar código. En ese caso, tendrá privilegios importantes sobre el contenedor en ejecución y podría acceder a los sistemas de archivos del host mediante montajes bind del sistema de archivos con permisos de acceso excesivamente amplios.

La forma más sencilla de evitarlo es crear un usuario específico, como en este ejemplo:

Ventana de terminal que muestra un Dockerfile que crea un directorio para la aplicación, agrega un usuario llamado brianvermeer, copia archivos y ejecuta la aplicación con ese usuario

En la tercera línea, creo un grupo nuevo y agrego un usuario. Este usuario es una cuenta del sistema (-r) sin contraseña ni directorio de inicio. También lo agrego al grupo que acabo de crear.

Luego, en la línea 6, le doy al usuario permiso para acceder a la carpeta de la aplicación. No olvides la línea 7. Aquí establezco el usuario que quiero usar. Así, el usuario restringido que acabo de crear ejecuta el comando de la última línea.

Analiza tu imagen Docker y tu aplicación Java durante el desarrollo

Crear una imagen Docker a partir de un Dockerfile, e incluso volver a compilar una imagen, puede introducir nuevas vulnerabilidades en tu sistema. Analizar tus imágenes Docker durante el desarrollo debería ser parte de tu flujo de trabajo para detectar vulnerabilidades lo antes posible.

Puedes analizar fácilmente tu imagen Docker con la CLI de Snyk. Úsala en tu equipo local, como parte de tu pipeline o de ambas formas. Después de instalar y autenticar la CLI de Snyk, solo tienes que hacer lo siguiente para analizar una imagen:

$ snyk container test <imageName>

Si quiero analizar una imagen de adoptopenjdk, como mencioné en la primera sección, los comandos serían los siguientes:

$ docker pull adoptopenjdk/opendjdk11:latest
$ snyk container test adoptopenjdk/opendjdk11:latest

Salida:

Terminal que muestra un análisis de Snyk de una imagen Docker OpenJDK, con vulnerabilidades de gravedad media y 21 problemas encontrados entre 132 dependencias.

Puedes probar y monitorear la imagen Docker. Para monitorearla, usa snyk container monitor <image>. El monitoreo toma una instantánea y verifica si aparecen nuevas vulnerabilidades o correcciones para tu imagen con el paso del tiempo.

Cuando analices una imagen y tengas el Dockerfile (porque creaste una nueva imagen Docker para Java), debes agregar la opción --Dockerfile=<dockerfile> a snyk container test o snyk container monitor. Así recibirás mejores recomendaciones para corregir los problemas. Por ejemplo, sabrás si hay una imagen base disponible que reduzca la cantidad de vulnerabilidades.

Ejemplo:

$ snyk container test myImage:mytag --Dockerfile=path/Dockerfile
$ snyk container monitor myImage:mytag --Dockerfile=path/Dockerfile

Analiza tu aplicación Java

La imagen Docker para Java que estás creando también contiene tu aplicación. Como es lógico, este también puede ser un punto de ataque. Debes asegurarte de que tu aplicación Java no tenga vulnerabilidades de seguridad para que Docker sea una opción segura para los desarrolladores de Java desde el principio. Imagina que tu aplicación contiene una biblioteca que permite la ejecución remota de código al llamar a un endpoint REST. Aunque el resto de la imagen no tenga vulnerabilidades, esto podría ser desastroso.

La mayor parte del binario de Java que incluyes en tu imagen Docker probablemente sea código importado. Puedes considerar las bibliotecas y los frameworks que usa tu aplicación como dependencias. Revisarlas es fácil con la CLI de Snyk. Es la misma CLI que usamos antes para analizar nuestra imagen. Ejecuta snyk test o snyk monitor en la carpeta raíz para analizar o monitorear tu aplicación en busca de vulnerabilidades de seguridad en sus bibliotecas.

Para el código que escribiste, es recomendable usar una herramienta de análisis de código o un linter, como SonarLint, PMD o SpotBugs. Estas herramientas son de uso general y sirven para crear código de mejor calidad, pero también te ayudan a evitar errores de seguridad evidentes.

Compila para volver a compilar

Compila tu aplicación Java para la imagen Docker de forma que puedas descartarla y volver a compilarla en cualquier momento. Supongamos que detectas un problema con tu contenedor en ejecución. Sería ideal poder detenerlo y poner en marcha una nueva instancia. Esto significa que debes diseñar aplicaciones Java sin estado, de modo que los datos se almacenen fuera del contenedor. Algunas cosas que puedes tener en cuenta son:

  • no ejecutes un almacén de datos o una base de datos en tu contenedor.

  • no almacenes archivos (de registro) en tu contenedor

  • asegúrate de que la caché se recupere automáticamente (si corresponde)

Si compilas tu aplicación de forma que puedas descartarla y lanzar una nueva instancia en cualquier momento, también podrás volver a compilar de forma segura toda la imagen Docker. ¿Sabías que puedes corregir uno o varios problemas de seguridad con solo volver a compilar la imagen en el 20 % de las imágenes Docker vulnerables? En muchos casos, las imágenes Docker se basan en la etiqueta “latest” de una imagen base. Estas versiones “latest” cambian con el tiempo y se reemplazan por versiones más nuevas y mejoradas. Lo mismo ocurre con los binarios clave instalados en tu contenedor mediante administradores de paquetes como apt o yum. Por supuesto, usar la versión más reciente es bueno desde el punto de vista de la seguridad, ya que recibirás automáticamente las últimas correcciones de seguridad. Sin embargo, debes tener en cuenta que la imagen base cambiará con el tiempo y que, por eso, será más difícil recrear tu imagen tal como estaba en un momento específico.

Aunque tu aplicación no haya cambiado, vuelve a compilar periódicamente tu imagen Docker, posiblemente con una etiqueta de versión más nueva o más reciente de la imagen base. Las mejoras en las capas subyacentes, como la del sistema operativo, pueden mejorar la calidad de tu imagen y reducir las vulnerabilidades de seguridad.

https://www.youtube.com/watch?v=v2SkWn-ZRDg

En resumen, si quieres mantenerte al día con las prácticas recomendadas de seguridad para crear imágenes Docker óptimas, ya sea en general o para aplicaciones Java:

  1. 10 prácticas recomendadas para crear un contenedor Java con Docker: una guía detallada, paso a paso, que te muestra cómo crear imágenes Docker seguras y eficientes para tus aplicaciones Java

  2. 10 prácticas recomendadas de seguridad para Java: prácticas de seguridad que debes seguir al crear aplicaciones Java para cualquier entorno.

Comienza con Capture the Flag

Aprende a resolver desafíos de Capture the Flag con nuestro taller virtual 101 a pedido.