Métodos y ejemplos de implementación de utilidades para comprobar la seguridad de Docker

Métodos y ejemplos de implementación de utilidades para comprobar la seguridad de Docker
¡Hola, Habr!

En la realidad actual, debido al creciente papel de la contenerización en los procesos de desarrollo, la cuestión de asegurar las diversas etapas y entidades relacionadas con los contenedores no es de poca importancia. Realizar verificaciones manualmente es una tarea laboriosa, por lo que sería ideal dar al menos los primeros pasos hacia la automatización de este proceso.

En este artículo compartiré scripts listos para implementar varias utilidades de seguridad para Docker y una guía sobre cómo desplegar un pequeño entorno de demostración para verificar este proceso. Estos materiales pueden ser utilizados para experimentar sobre cómo organizar el proceso de prueba de seguridad de las imágenes y las instrucciones de Dockerfile. Es evidente que la infraestructura de desarrollo y despliegue de cada uno es diferente, por lo que a continuación presentaré algunas opciones posibles.

Utilidades de verificación de seguridad

Existen muchas aplicaciones complementarias y scripts diferentes que realizan verificaciones sobre diversos aspectos de la infraestructura de Docker. Algunas de ellas ya fueron descritas en el artículo anterior (https://habr.com/ru/company/swordfish_security/blog/518758/#docker-security), y en este material me gustaría centrarme en tres de ellas que cubren la mayor parte de los requisitos de seguridad de las imágenes de Docker que se construyen en el proceso de desarrollo. Además, también mostraré un ejemplo de cómo se pueden conectar estas tres utilidades en un solo pipeline para llevar a cabo las verificaciones de seguridad.

Hadolint
https://github.com/hadolint/hadolint

Una utilidad de consola bastante sencilla que ayuda a evaluar de manera preliminar la corrección y seguridad de las instrucciones de los Dockerfiles (por ejemplo, el uso solamente de registros de imágenes permitidos o el uso de sudo).

Métodos y ejemplos de implementación de utilidades para comprobar la seguridad de Docker

Dockle
https://github.com/goodwithtech/dockle

Una utilidad de consola que trabaja con una imagen (o con un archivo tar guardado de la imagen) que verifica la corrección y seguridad de una imagen específica como tal, analizando sus capas y configuración: qué usuarios se han creado, qué instrucciones se utilizan, qué volúmenes están conectados, la presencia de contraseñas vacías, etc. Por el momento, el número de verificaciones no es muy grande y se basa en algunas verificaciones y recomendaciones propias. CIS (Center for Internet Security) Benchmark para Docker.
Métodos y ejemplos de implementación de utilidades para comprobar la seguridad de Docker

Trivy
https://github.com/aquasecurity/trivy

Esta herramienta está diseñada para encontrar vulnerabilidades de dos tipos: problemas en las compilaciones del sistema operativo (se admiten Alpine, RedHat (EL), CentOS, Debian GNU, Ubuntu) y problemas en las dependencias (Gemfile.lock, Pipfile.lock, composer.lock, package-lock.json, yarn.lock, Cargo.lock). Trivy puede escanear tanto una imagen en el repositorio como una imagen local, y también realizar escaneos basados en un archivo .tar que contenga una imagen de Docker.

Métodos y ejemplos de implementación de utilidades para comprobar la seguridad de Docker

Opciones para integrar las herramientas

Para probar las aplicaciones descritas en un entorno aislado, proporcionaré instrucciones sobre cómo instalar todas las herramientas en un proceso simplificado.

La idea principal es demostrar cómo se puede implementar una verificación automática del contenido del Dockerfile y de las imágenes de Docker que se crean durante el proceso de desarrollo.

La verificación en sí consiste en los siguientes pasos:

  1. Verificación de la corrección y seguridad de las instrucciones del Dockerfile con la herramienta linter Hadolint
  2. Verificación de la corrección y seguridad de las imágenes finales e intermedias con la herramienta Dockle
  3. Verificación de la existencia de vulnerabilidades conocidas (CVE) en la imagen base y en varias dependencias con la herramienta Trivy

A continuación, en el artículo presentaré tres opciones para implementar estos pasos:
La primera es configurando un pipeline de CI/CD utilizando GitLab (con una descripción del proceso para levantar una instancia de prueba).
La segunda es utilizando un script de shell.
La tercera es construyendo una imagen de Docker para escanear imágenes de Docker.
Puedes elegir la opción que más te convenga, trasladarla a tu infraestructura y adaptarla a tus necesidades.

Todos los archivos necesarios y las instrucciones adicionales también se encuentran en el repositorio: https://github.com/Swordfish-Security/docker_cicd

Integración en GitLab CI/CD

En la primera opción, veremos cómo se pueden implementar comprobaciones de seguridad tomando como ejemplo el sistema de repositorios de GitLab. Aquí seguiremos los pasos y explicaremos cómo instalar desde cero un entorno de prueba con GitLab, elaborar un proceso de escaneo y ejecutar las herramientas para verificar un Dockerfile de prueba y una imagen aleatoria: la aplicación JuiceShop.

Instalación de GitLab
1. Instalamos Docker:

sudo apt-get update && sudo apt-get install docker.io

2. Agregamos el usuario actual al grupo docker para poder trabajar con Docker sin usar sudo:

sudo addgroup  docker

3. Encontramos nuestra IP:

ip addr

4. Instalamos y ejecutamos GitLab en un contenedor, reemplazando la dirección IP en hostname por la nuestra:

docker run --detach 
--hostname 192.168.1.112 
--publish 443:443 --publish 80:80 
--name gitlab 
--restart always 
--volume /srv/gitlab/config:/etc/gitlab 
--volume /srv/gitlab/logs:/var/log/gitlab 
--volume /srv/gitlab/data:/var/opt/gitlab 
gitlab/gitlab-ce:latest

Esperamos a que GitLab realice todos los procedimientos necesarios de instalación (se puede seguir el proceso a través de la salida del archivo de registro: docker logs -f gitlab).

5. Abrimos en el navegador nuestra IP local y vemos la página con la propuesta de cambiar la contraseña para el usuario root:
Métodos y ejemplos de implementación de utilidades para comprobar la seguridad de Docker
Establecemos una nueva contraseña y accedemos a GitLab.

6. Creamos un nuevo proyecto, por ejemplo, cicd-test y lo inicializamos con un archivo de inicio README.md:
Métodos y ejemplos de implementación de utilidades para comprobar la seguridad de Docker
7. Ahora necesitamos instalar GitLab Runner: el agente que ejecutará todas las operaciones necesarias bajo demanda.
Descargamos la última versión (en este caso — para Linux de 64 bits):

sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64

8. Lo hacemos ejecutable:

sudo chmod +x /usr/local/bin/gitlab-runner

9. Añadimos un usuario del sistema para el Runner y iniciamos el servicio:

sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash
sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
sudo gitlab-runner start

Debería verse algo así:

local@osboxes:~$ sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
Runtime platform arch=amd64 os=linux pid=8438 revision=0e5417a3 version=12.0.1
local@osboxes:~$ sudo gitlab-runner start
Runtime platform arch=amd64 os=linux pid=8518 revision=0e5417a3 version=12.0.1

10. Ahora registramos el Runner para que pueda interactuar con nuestra instancia de GitLab.
Para ello, abrimos la página Configuración-CI/CD (http://OUR_IP_ADDRESS/root/cicd-test/-/settings/ci_cd) y en la pestaña Runners encontramos la URL y el Token de Registro:
Métodos y ejemplos de implementación de utilidades para comprobar la seguridad de Docker
11. Registramos el Runner, insertando la URL y el Token de Registro:

sudo gitlab-runner register 
--non-interactive 
--url "http:///" 
--registration-token "" 
--executor "docker" 
--docker-privileged 
--docker-image alpine:latest 
--description "docker-runner" 
--tag-list "docker,privileged" 
--run-untagged="true" 
--locked="false" 
--access-level="not_protected"

Como resultado, obtenemos un GitLab funcional, al que es necesario añadir instrucciones para el inicio de nuestras herramientas. En este caso de demostración, no tenemos pasos de construcción de la aplicación y su contenedorización, pero en un entorno real, estos precederán a los pasos de escaneo y generarán imágenes y Dockerfile para análisis.

Configuración del pipeline

1. Añadiremos archivos al repositorio mydockerfile.df (este es un Dockerfile de prueba que vamos a verificar) y el archivo de configuración del proceso GitLab CI/CD .gitlab-cicd.yml, que enumera las instrucciones para los escáneres (preste atención al punto en el nombre del archivo).

El archivo de configuración YAML contiene instrucciones para ejecutar tres utilidades (Hadolint, Dockle y Trivy), que analizarán el Dockerfile seleccionado y la imagen especificada en la variable DOCKERFILE. Todos los archivos necesarios se pueden obtener del repositorio: https://github.com/Swordfish-Security/docker_cicd/

Extracto de mydockerfile.df (este es un archivo abstracto con un conjunto de instrucciones arbitrarias solo para demostrar el funcionamiento de la utilidad). Enlace directo al archivo: mydockerfile.df

Contenido de mydockerfile.df

FROM amd64/node:10.16.0-alpine@sha256:f59303fb3248e5d992586c76cc83e1d3700f641cbcd7c0067bc7ad5bb2e5b489 AS tsbuild
COPY package.json .
COPY yarn.lock .
RUN yarn install
COPY lib lib
COPY tsconfig.json tsconfig.json
COPY tsconfig.app.json tsconfig.app.json
RUN yarn build
FROM amd64/ubuntu:18.04@sha256:eb70667a801686f914408558660da753cde27192cd036148e58258819b927395
LABEL maintainer="Rhys Arkins "
LABEL name="renovate"
...
COPY php.ini /usr/local/etc/php/php.ini
RUN cp -a /tmp/piik/* /var/www/html/
RUN rm -rf /tmp/piwik
RUN chown -R www-data /var/www/html
ADD piwik-cli-setup /piwik-cli-setup
ADD reset.php /var/www/html/
## ENTRYPOINT ##
ADD entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
USER root

La configuración YAML se ve así (el archivo se puede obtener a través del enlace directo aquí: .gitlab-ci.yml):

Contenido de .gitlab-ci.yml

variables:
    DOCKER_HOST: "tcp://docker:2375/"
    DOCKERFILE: "mydockerfile.df" # nombre del Dockerfile a analizar   
    DOCKERIMAGE: "bkimminich/juice-shop" # nombre de la imagen Docker a analizar
    # DOCKERIMAGE: "knqyf263/cve-2018-11235" # imagen Docker de prueba con varias CVE CRÍTICAS
    SHOWSTOPPER_PRIORITY: "CRÍTICO" # qué nivel de criticidad fallará el trabajo Trivy
    TRIVYCACHE: "$CI_PROJECT_DIR/.cache" # donde almacenar la base de datos de vulnerabilidades de Trivy para un uso más rápido
    ARTIFACT_FOLDER: "$CI_PROJECT_DIR"
 
services:
    - docker:dind # para poder construir imágenes de Docker dentro del Runner
 
stages:
    - escanear
    - informe
    - publicar
 
HadoLint:
    # Análisis básico de lint de instrucciones de Dockerfile
    stage: escanear
    image: docker:git
 
    after_script:
    - cat $ARTIFACT_FOLDER/hadolint_results.json
 
    script:
    - export VERSION=$(wget -q -O - https://api.github.com/repos/hadolint/hadolint/releases/latest | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*//1/')
    - wget https://github.com/hadolint/hadolint/releases/download/v${VERSION}/hadolint-Linux-x86_64 && chmod +x hadolint-Linux-x86_64
     
    # NB: hadolint siempre saldrá con código de salida 0
    - ./hadolint-Linux-x86_64 -f json $DOCKERFILE > $ARTIFACT_FOLDER/hadolint_results.json || exit 0
 
    artifacts:
        when: always # devolver artefactos incluso después de fallar el trabajo       
        paths:
        - $ARTIFACT_FOLDER/hadolint_results.json
 
Dockle:
    # Analizando las mejores prácticas sobre la imagen Docker (permisos de usuario, instrucciones seguidas al construir la imagen, etc.)
    stage: escanear   
    image: docker:git
 
    after_script:
    - cat $ARTIFACT_FOLDER/dockle_results.json
 
    script:
    - export VERSION=$(wget -q -O - https://api.github.com/repos/goodwithtech/dockle/releases/latest | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*//1/')
    - wget https://github.com/goodwithtech/dockle/releases/download/v${VERSION}/dockle_${VERSION}_Linux-64bit.tar.gz && tar zxf dockle_${VERSION}_Linux-64bit.tar.gz
    - ./dockle --exit-code 1 -f json --output $ARTIFACT_FOLDER/dockle_results.json $DOCKERIMAGE   
     
    artifacts:
        when: always # devolver artefactos incluso después de fallar el trabajo       
        paths:
        - $ARTIFACT_FOLDER/dockle_results.json
 
Trivy:
    # Analizando la imagen Docker y las dependencias de paquetes contra varias bases de CVE
    stage: escanear   
    image: docker:git
 
    script:
    # obtener la última versión de Trivy
    - apk add rpm
    - export VERSION=$(wget -q -O - https://api.github.com/repos/knqyf263/trivy/releases/latest | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*//1')
    - wget https://github.com/knqyf263/trivy/releases/download/v${VERSION}/trivy_${VERSION}_Linux-64bit.tar.gz && tar zxf trivy_${VERSION}_Linux-64bit.tar.gz
     
    # mostrando todas las vulnerabilidades sin fallar la construcción
    - ./trivy -d --cache-dir $TRIVYCACHE -f json -o $ARTIFACT_FOLDER/trivy_results.json --exit-code 0 $DOCKERIMAGE    
    
    # escribir información de vulnerabilidades en stdout en formato legible (leer puro json no es divertido, ¿eh?). Puedes eliminar esto si no lo necesitas.
    - ./trivy -d --cache-dir $TRIVYCACHE --exit-code 0 $DOCKERIMAGE    
 
    # fallar la construcción si se encuentra la prioridad SHOWSTOPPER
    - ./trivy -d --cache-dir $TRIVYCACHE --exit-code 1 --severity $SHOWSTOPPER_PRIORITY --quiet $DOCKERIMAGE
         
    artifacts:
        when: always # devolver artefactos incluso después de fallar el trabajo
        paths:
        - $ARTIFACT_FOLDER/trivy_results.json
 
    cache:
        paths:
        - .cache
 
Informe:
    # combinando las salidas de herramientas en un HTML
    stage: informe
    when: always
    image: python:3.5
     
    script:
    - mkdir json
    - cp $ARTIFACT_FOLDER/*.json ./json/
    - pip install json2html
    - wget https://raw.githubusercontent.com/shad0wrunner/docker_cicd/master/convert_json_results.py
    - python ./convert_json_results.py
     
    artifacts:
        paths:
        - results.html

Si es necesario, también se pueden escanear y guardar imágenes como un archivo .tar (sin embargo, será necesario cambiar los parámetros de entrada para las utilidades en el archivo YAML).

NB: Trivy requiere que se instale para su ejecución. rpm y git. De lo contrario, devolverá errores al escanear imágenes basadas en RedHat y al obtener actualizaciones de la base de vulnerabilidades.

2. Después de agregar archivos al repositorio, de acuerdo con las instrucciones en nuestro archivo de configuración, GitLab comenzará automáticamente el proceso de construcción y escaneo. En la pestaña CI/CD → Pipelines se podrá ver el progreso de la ejecución de las instrucciones.

Como resultado, tenemos cuatro tareas. Tres de ellas se encargan directamente del escaneo y la última (Report) recopila un informe simple a partir de archivos dispares con los resultados del escaneo.
Métodos y ejemplos de implementación de utilidades para comprobar la seguridad de Docker
Por defecto, Trivy detiene su ejecución si se detectan vulnerabilidades CRÍTICAS en la imagen o sus dependencias. Al mismo tiempo, Hadolint siempre devuelve un código de éxito, ya que siempre hay observaciones en el resultado de su ejecución, lo que lleva a la detención de la construcción.

Dependiendo de los requisitos específicos, se puede configurar el código de salida para que estas utilidades detengan también el proceso de construcción al detectar problemas de cierta criticidad. En nuestro caso, la construcción se detendrá solo si Trivy detecta una vulnerabilidad con la criticidad que hemos especificado en la variable SHOWSTOPPER en. .gitlab-ci.yml.
Métodos y ejemplos de implementación de utilidades para comprobar la seguridad de Docker

El resultado del trabajo de cada utilidad se puede ver en el registro de cada tarea de escaneo, directamente en los archivos json en la sección de artifacts o en un informe HTML simple (de eso hablaremos más adelante):
Métodos y ejemplos de implementación de utilidades para comprobar la seguridad de Docker

3. Para presentar los informes de las utilidades en un formato un poco más legible, se utiliza un pequeño script en Python para convertir tres archivos json en un archivo HTML con una tabla de defectos.
Este script se ejecuta como una tarea separada llamada Report, y su artefacto final es un archivo HTML con el informe. El código fuente del script también está en el repositorio y se puede adaptar a sus necesidades, colores, etc.
Métodos y ejemplos de implementación de utilidades para comprobar la seguridad de Docker

Script Shell

La segunda opción es adecuada para casos en los que es necesario verificar las imágenes de Docker fuera del entorno de una CI/CD, o cuando se requiere tener todas las instrucciones en un formato que se pueda ejecutar directamente en el host. Esta opción se cubre con un script shell predefinido que se puede ejecutar en una máquina virtual (o incluso real) limpia. El script ejecuta las mismas instrucciones que el gitlab-runner descrito anteriormente.

Para que el script funcione correctamente, se debe tener Docker instalado en el sistema y el usuario actual debe pertenecer al grupo docker.

El script puede descargarse aquí: docker_sec_check.sh

Al inicio del archivo, se definen las variables que especifican qué imagen se debe escanear y qué defectos de qué criticidad causarán la salida de la utilidad Trivy con el código de error especificado.

Durante la ejecución del script, todas las utilidades se descargarán en el directorio docker_tools, y los resultados de su trabajo estarán en el directorio docker_tools/json, mientras que el HTML del informe se encontrará en el archivo results.html.

Ejemplo de salida del script

~~/docker_cicd$ ./docker_sec_check.sh

[+] Configurando variables de entorno
[+] Instalando paquetes requeridos
[+] Preparando directorios necesarios
[+] Obteniendo Dockerfile de muestra
2020-10-20 10:40:00 (45.3 MB/s) - ‘Dockerfile’ guardado [8071/8071]
[+] Descargando imagen para escanear
latest: Descargando de bkimminich/juice-shop
[+] Ejecutando Hadolint
...
Dockerfile:205 DL3015 Evita paquetes adicionales especificando `--no-install-recommends`
Dockerfile:248 DL3002 El último usuario no debe ser root
...
[+] Ejecutando Dockle
...
WARN - DKL-DI-0006: Evita la etiqueta latest
        * Evita la etiqueta 'latest'
INFO - CIS-DI-0005: Habilita la confianza de contenido para Docker
        * exporta DOCKER_CONTENT_TRUST=1 antes de docker pull/build
...
[+] Ejecutando Trivy
juice-shop/frontend/package-lock.json
=====================================
Total: 3 (UNKNOWN: 0, LOW: 1, MEDIUM: 0, HIGH: 2, CRITICAL: 0)

+---------------------+------------------+----------+---------+-------------------------+
|       LIBRERÍA      | ID DE VULNERABILIDAD | SEVERIDAD | VERSIÓN |             TÍTULO      |
+---------------------+------------------+----------+---------+-------------------------+
| object-path         | CVE-2020-15256   | ALTA     | 0.11.4  | Polución de prototipo en |
|                     |                  |          |         | object-path             |
+---------------------+------------------+          +---------+-------------------------+
| tree-kill           | CVE-2019-15599   |          | 1.2.2   | Inyección de código      |
+---------------------+------------------+----------+---------+-------------------------+
| webpack-subresource | CVE-2020-15262   | BAJA     | 1.4.1   | Fragmentos cargados dinámicamente sin protección |
|                     |                  |          |         |                        |
+---------------------+------------------+----------+---------+-------------------------+

juice-shop/package-lock.json
============================
Total: 20 (UNKNOWN: 0, LOW: 1, MEDIUM: 6, HIGH: 8, CRITICAL: 5)

...

juice-shop/package-lock.json
============================
Total: 5 (CRITICAL: 5)

...
[+] Eliminando residuos
[+] Haciendo que la salida se vea bien
[+] Convirtiendo resultados JSON
[+] Escribiendo resultados HTML
[+] Salida limpia ============================================================
[+] Todo está hecho. Encuentra el informe HTML resultante en results.html

Imagen de Docker con todas las utilidades

Como tercera alternativa, he creado dos Dockerfiles sencillos para construir una imagen con utilidades de seguridad. Un Dockerfile ayudará a crear un conjunto para escanear la imagen desde el repositorio, el segundo (Dockerfile_tar) permitirá construir un conjunto para escanear un archivo tar con la imagen.

1. Tomamos el archivo Docker correspondiente y los scripts del repositorio https://github.com/Swordfish-Security/docker_cicd/tree/master/Dockerfile.
2. Lo ejecutamos para construir:

docker build -t dscan:image -f docker_security.df .

3. Después de que la construcción finalice, creamos un contenedor a partir de la imagen. Al mismo tiempo, pasamos la variable de entorno DOCKERIMAGE con el nombre de la imagen que nos interesa y montamos el Dockerfile que queremos analizar desde nuestra máquina en un archivo /Dockerfile (tenga en cuenta que se requiere la ruta absoluta hasta este archivo):

docker run --rm -v $(pwd)\/results:\/results -v $(pwd)\/docker_security.df:\/Dockerfile -e DOCKERIMAGE="bkimminich\/juice-shop" dscan:image


[+] Estableciendo variables de entorno
[+] Ejecutando Hadolint
\/Dockerfile:3 DL3006 Siempre etiqueta explícitamente la versión de una imagen
[+] Ejecutando Dockle
WARN - DKL-DI-0006: Evitar la etiqueta latest
        * Evitar la etiqueta 'latest'
INFO - CIS-DI-0005: Habilitar la confianza de contenido para Docker
        * export DOCKER_CONTENT_TRUST=1 antes de docker pull\/build
INFO - CIS-DI-0006: Añadir la instrucción HEALTHCHECK a la imagen del contenedor
        * no se encontró la declaración HEALTHCHECK
INFO - DKL-LI-0003: Solo poner archivos necesarios
        * archivo innecesario: juice-shop\/node_modules\/sqlite3\/Dockerfile
        * archivo innecesario: juice-shop\/node_modules\/sqlite3\/tools\/docker\/architecture\/linux-arm64\/Dockerfile
        * archivo innecesario: juice-shop\/node_modules\/sqlite3\/tools\/docker\/architecture\/linux-arm\/Dockerfile
[+] Ejecutando Trivy
...\njuice-shop\/package-lock.json
============================
Total: 20 (UNKNOWN: 0, LOW: 1, MEDIUM: 6, HIGH: 8, CRITICAL: 5)
...\n[+] Haciendo que la salida luzca bonita
[+] Iniciando el módulo principal ============================================================
[+] Convirtiendo resultados JSON
[+] Escribiendo resultados en HTML
[+] Salida limpia ============================================================
[+] Todo está hecho. Encuentra el informe HTML resultante en results.html

Resultados

Solo hemos considerado un conjunto básico de utilidades para escanear artefactos de Docker, que, en mi opinión, cubre de manera efectiva una buena parte de los requisitos de seguridad de las imágenes. Hay una gran cantidad de herramientas de pago y gratuitas que pueden realizar las mismas verificaciones, generar informes atractivos o funcionar únicamente en modo consola, abarcando sistemas de gestión de contenedores, etc. Una revisión de estas herramientas y métodos de integración puede aparecer más adelante.

Una de las ventajas del conjunto de herramientas descrito en el artículo es que todas están basadas en código abierto, lo que te permite experimentar con ellas y con otras herramientas similares para encontrar lo que se adapta a tus requerimientos y características de infraestructura. Sin duda, todas las vulnerabilidades que se encuentren deben ser estudiadas en función de su aplicabilidad en condiciones específicas, pero ese es un tema para un gran artículo futuro.

Espero que esta guía, los scripts y las utilidades te ayuden y se conviertan en un punto de partida para crear una infraestructura más segura en el ámbito de la contenedorización.

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster