Привет, Хабр!

В современной реальности из-за возрастающей роли контейнеризации в процессах разработки не на последнем месте стоит вопрос обеспечения безопасности различных этапов и сущностей, связанных с контейнерами. Осуществление проверок в ручном режиме является трудоёмким занятием, поэтому было бы неплохо сделать хотя бы начальные шаги к автоматизации этого процесса.

В этой статье я поделюсь готовыми скриптами для внедрения нескольких утилит обеспечения безопасности Docker и инструкцией, как развернуть небольшой демо-стенд для проверки этого процесса. Материалами можно воспользоваться, чтобы поэкспериментировать с тем, как организовать процесс тестирования безопасности образов и инструкций Dockerfile. Понятно, что инфраструктура разработки и внедрения у всех разные, поэтому ниже я приведу несколько возможных вариантов.

Утилиты проверки безопасности


Существует большое количество различных вспомогательных приложений и скриптов, которые выполняют проверки разнообразных аспектов Docker-инфраструктуры. Часть из них уже была описана в предыдущей статье (https://habr.com/ru/company/swordfish_security/blog/518758/#docker-security), а в данном материале я бы хотел остановиться на трех из них, которые покрывают основную часть требований к безопасности Docker-образов, строящихся в процессе разработки. Помимо этого я также покажу пример как можно эти три утилиты соединить в один pipeline для осуществления проверок безопасности.

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

Довольно простая консольная утилита, которая помогает в первом приближении оценить корректность и безопасность инструкций Dockerfile-ов (например использование только разрешенных реестров образов или использование sudo).

Вывод утилиты Hadolint

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

Консольная утилита, работающая с образом (или с сохраненным tar-архивом образа), которая проверяет корректность и безопасность конкретного образа как такового, анализируя его слои и конфигурацию – какие пользователи созданы, какие инструкции используются, какие тома подключены, присутствие пустого пароля и т. д. Пока количество проверок не очень большое и базируется на нескольких собственных проверках и рекомендациях CIS (Center for Internet Security) Benchmark для Docker.


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

Эта утилита нацелена на нахождение уязвимостей двух типов – проблемы сборок ОС (поддерживаются Alpine, RedHat (EL), CentOS, Debian GNU, Ubuntu) и проблемы в зависимостях (Gemfile.lock, Pipfile.lock, composer.lock, package-lock.json, yarn.lock, Cargo.lock). Trivy умеет сканировать как образ в репозитории, так и локальный образ, а также проводить сканирование на основании переданного .tar файла с Docker-образом.



Варианты внедрения утилит


Для того чтобы попробовать в изолированных условиях описанные приложения, я приведу инструкции по установке всех утилит в рамках некоторого упрощённого процесса.

Основная идея состоит в том, чтобы продемонстрировать, как можно внедрить автоматическую проверку содержимого Dockerfile и Docker-образов, которые создаются в процессе разработки.

Сама проверка состоит из следующих шагов:
  1. Проверка корректности и безопасности инструкций Dockerfile — утилитой линтером Hadolint
  2. Проверка корректности и безопасности конечного и промежуточных образов — утилитой Dockle
  3. Проверка наличия общеизвестных уязвимостей (CVE) в базовом образе и ряде зависимостей — утилитой Trivy

Дальше в статье я приведу три варианта внедрения этих шагов:
Первый — путём конфигурации CI/CD pipeline на примере GitLab (с описанием процесса поднятия тестового инстанса).
Второй — с использованием shell-скрипта.
Третий — с построением Docker-образа для сканирования Docker-образов.
Вы можете выбрать вариант который больше вам подходит, перенести его на свою инфраструктуру и адаптировать под свои нужды.

Все необходимые файлы и дополнительные инструкции также находятся в репозитории: https://github.com/Swordfish-Security/docker_cicd

Интеграция в GitLab CI/CD


В первом варианте мы рассмотрим, как можно внедрить проверки безопасности на примере системы репозиториев GitLab. Здесь мы пройдем по шагам и разберем как установить с нуля тестовое окружение с GitLab, составить процесс сканирования и осуществить запуск утилит для проверки тестового Dockerfile и случайного образа — приложения JuiceShop.

Установка GitLab
1. Ставим Docker:
sudo apt-get update && sudo apt-get install docker.io

2. Добавляем текущего пользователя в группу docker, чтобы можно было работать с докером не через sudo:
sudo addgroup <username> docker

3. Находим свой IP:
ip addr

4. Ставим и запускаем GitLab в контейнере, заменяя IP адрес в hostname на свой:
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

Ждём, пока GitLab выполнит все необходимые процедуры по установке (можно следить за процессом через вывод лог-файла: docker logs -f gitlab).

5. Открываем в браузере свой локальный IP и видим страницу с предложением поменять пароль для пользователя root:

Задаём новый пароль и заходим в GitLab.

6. Создаём новый проект, например cicd-test и инициализируем его стартовым файлом README.md:

7. Теперь нам необходимо установить GitLab Runner: агента, который будет по запросу запускать все необходимые операции.
Скачиваем последнюю версию (в данном случае — под Linux 64-bit):
sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64

8. Делаем его исполняемым:
sudo chmod +x /usr/local/bin/gitlab-runner

9. Добавляем пользователя ОС для Runner-а и запускаем сервис:
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

Должно получиться примерно так:

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. Теперь регистрируем Runner, чтобы он мог взаимодействовать с нашим инстансом GitLab.
Для этого открываем страницу Settings-CI/CD (http://OUR_ IP_ADDRESS/root/cicd-test/-/settings/ci_cd) и на вкладке Runners находим URL и Registration token:

11. Регистрируем Runner, подставляя URL и Registration token:
sudo gitlab-runner register --non-interactive --url "http://<URL>/" --registration-token "<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"

В итоге мы получаем готовый работающий GitLab, в который необходимо добавить инструкции для старта наших утилит. В данном демонстрационном случае у нас нет шагов сборки приложения и его контейнеризации, но в реальном окружении они будут предшествовать шагам сканирования и формировать образы и Dockerfile для анализа.

Конфигурация pipeline

1. Добавим в репозиторий файлы mydockerfile.df (это некий тестовый Dockerfile, который мы будем проверять) и конфигурационный файл GitLab CI/CD процесса .gitlab-cicd.yml, который перечисляет инструкции для сканеров (обратите внимание на точку в названии файла).

YAML-файл конфигурации содержит инструкции по запуску трех утилит (Hadolint, Dockle и Trivy), которые проанализируют выбранный Dockerfile и образ, заданный в переменной DOCKERFILE. Все необходимые файлы можно взять из репозитория: https://github.com/Swordfish-Security/docker_cicd/

Выдержка из mydockerfile.df (это абстрактный файл с набором произвольных инструкций только для демонстрации работы утилиты). Прямая ссылка на файл: mydockerfile.df

Содержимое 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 <rhys@arkins.net>"
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

Конфигурационный YAML выглядит таким образом (сам файл можно взять по прямой ссылке здесь: .gitlab-ci.yml):

Содержимое .gitlab-ci.yml
variables:
    DOCKER_HOST: "tcp://docker:2375/"
    DOCKERFILE: "mydockerfile.df" # name of the Dockerfile to analyse   
    DOCKERIMAGE: "bkimminich/juice-shop" # name of the Docker image to analyse
    # DOCKERIMAGE: "knqyf263/cve-2018-11235" # test Docker image with several CRITICAL CVE
    SHOWSTOPPER_PRIORITY: "CRITICAL" # what level of criticality will fail Trivy job
    TRIVYCACHE: "$CI_PROJECT_DIR/.cache" # where to cache Trivy database of vulnerabilities for faster reuse
    ARTIFACT_FOLDER: "$CI_PROJECT_DIR"
 
services:
    - docker:dind # to be able to build docker images inside the Runner
 
stages:
    - scan
    - report
    - publish
 
HadoLint:
    # Basic lint analysis of Dockerfile instructions
    stage: scan
    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 will always exit with 0 exit code
    - ./hadolint-Linux-x86_64 -f json $DOCKERFILE > $ARTIFACT_FOLDER/hadolint_results.json || exit 0
 
    artifacts:
        when: always # return artifacts even after job failure       
        paths:
        - $ARTIFACT_FOLDER/hadolint_results.json
 
Dockle:
    # Analysing best practices about docker image (users permissions, instructions followed when image was built, etc.)
    stage: scan   
    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 # return artifacts even after job failure       
        paths:
        - $ARTIFACT_FOLDER/dockle_results.json
 
Trivy:
    # Analysing docker image and package dependencies against several CVE bases
    stage: scan   
    image: docker:git
 
    script:
    # getting the latest 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
     
    # displaying all vulnerabilities w/o failing the build
    - ./trivy -d --cache-dir $TRIVYCACHE -f json -o $ARTIFACT_FOLDER/trivy_results.json --exit-code 0 $DOCKERIMAGE    
    
    # write vulnerabilities info to stdout in human readable format (reading pure json is not fun, eh?). You can remove this if you don't need this.
    - ./trivy -d --cache-dir $TRIVYCACHE --exit-code 0 $DOCKERIMAGE    
 
    # failing the build if the SHOWSTOPPER priority is found
    - ./trivy -d --cache-dir $TRIVYCACHE --exit-code 1 --severity $SHOWSTOPPER_PRIORITY --quiet $DOCKERIMAGE
         
    artifacts:
        when: always # return artifacts even after job failure
        paths:
        - $ARTIFACT_FOLDER/trivy_results.json
 
    cache:
        paths:
        - .cache
 
Report:
    # combining tools outputs into one HTML
    stage: report
    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

При необходимости также можно сканировать и сохраненные образы в виде .tar-архива (однако потребуется в YAML файле изменить входные параметры для утилит)

NB: Trivy требует для своего запуска установленные rpm и git. В противном случае он будет выдавать ошибки при сканировании RedHat-based образов и получении обновлений базы уязвимостей.

2. После добавления файлов в репозиторий, в соответствии с инструкциями в нашем конфигурационном файле, GitLab автоматически начнёт процесс сборки и сканирования. На вкладке CI/CD > Pipelines можно будет увидеть ход выполнения инструкций.

В результате у нас есть четыре задачи. Три из них занимаются непосредственно сканированием и последняя (Report) собирает простой отчёт из разрозненных файлов с результатами сканирования.

По умолчанию Trivy останавливает своё выполнение, если были обнаружены CRITICAL уязвимости в образе или зависимостях. В то же время Hadolint всегда возвращает Success код выполнения, так как в результате его выполнения всегда есть замечания, что приводит к остановке сборки.

В зависимости от конкретных требований можно сконфигурировать код выхода, чтобы эти утилиты при обнаружении проблем определенной критичности, останавливали также и процесс сборки. В нашем случае сборка остановится, только если Trivy обнаружит уязвимость с критичностью, которую мы указали в переменной SHOWSTOPPER в .gitlab-ci.yml.


Результат работы каждой утилиты можно посмотреть в логе каждой сканирующей задачи, непосредственно в json-файлах в разделе artifacts или в простом HTML-отчёте (о нем чуть ниже):


3. Для представления отчётов утилит в чуть более человекочитаемом виде используется небольшой скрипт на Python для конвертации трёх json-файлов в один HTML-файл с таблицей дефектов.
Этот скрипт запускается отдельной задачей Report, а его итоговым артефактом является HTML-файл с отчётом. Исходник скрипта также лежит в репозитории и его можно адаптировать под свои нужды, цвета и т. п.


Shell-скрипт


Второй вариант подходит для случаев, когда необходимо проверять Docker-образы не в рамках CI/CD системы или необходимо иметь все инструкции в виде, который можно выполнить непосредственно на хосте. Этот вариант покрывается готовым shell-скриптом, который можно запустить на чистой виртуальной (или даже реальной) машине. Скрипт выполняет те же самые инструкции, что и вышеописанный gitlab-runner.

Для успешной работы скрипта в системе должен быть установлен Docker и текущий пользователь должен быть в группе docker.

Сам скрипт можно взять здесь: docker_sec_check.sh

В начале файла переменными задается, какой образ необходимо сканировать и дефекты какой критичности будут вызывать выход из утилиты Trivy с указанным кодом ошибки.

В процессе выполнения скрипта все утилиты будут скачаны в директорию docker_tools, результаты их работы — в директорию docker_tools/json, а HTML с отчётом будет находиться в файле results.html.

Пример вывода скрипта
~/docker_cicd$ ./docker_sec_check.sh

[+] Setting environment variables
[+] Installing required packages
[+] Preparing necessary directories
[+] Fetching sample Dockerfile
2020-10-20 10:40:00 (45.3 MB/s) - ‘Dockerfile’ saved [8071/8071]
[+] Pulling image to scan
latest: Pulling from bkimminich/juice-shop
[+] Running Hadolint
...
Dockerfile:205 DL3015 Avoid additional packages by specifying `--no-install-recommends`
Dockerfile:248 DL3002 Last USER should not be root
...
[+] Running Dockle
...
WARN    - DKL-DI-0006: Avoid latest tag
        * Avoid 'latest' tag
INFO    - CIS-DI-0005: Enable Content trust for Docker
        * export DOCKER_CONTENT_TRUST=1 before docker pull/build
...
[+] Running Trivy
juice-shop/frontend/package-lock.json
=====================================
Total: 3 (UNKNOWN: 0, LOW: 1, MEDIUM: 0, HIGH: 2, CRITICAL: 0)

+---------------------+------------------+----------+---------+-------------------------+
|       LIBRARY       | VULNERABILITY ID | SEVERITY | VERSION |             TITLE       |
+---------------------+------------------+----------+---------+-------------------------+
| object-path         | CVE-2020-15256   | HIGH     | 0.11.4  | Prototype pollution in  |
|                     |                  |          |         | object-path             |
+---------------------+------------------+          +---------+-------------------------+
| tree-kill           | CVE-2019-15599   |          | 1.2.2   | Code Injection          |
+---------------------+------------------+----------+---------+-------------------------+
| webpack-subresource | CVE-2020-15262   | LOW      | 1.4.1   | Unprotected dynamically |
|                     |                  |          |         | loaded chunks           |
+---------------------+------------------+----------+---------+-------------------------+

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)

...
[+] Removing left-overs
[+] Making the output look pretty
[+] Converting JSON results
[+] Writing results HTML
[+] Clean exit ============================================================
[+] Everything is done. Find the resulting HTML report in results.html


Docker-образ со всеми утилитами


В качестве третьей альтернативы я составил два простых Dockerfile для создания образа с утилитами безопасности. Один Dockerfile поможет собрать набор для сканирования образа из репозитория, второй (Dockerfile_tar) — собрать набор для сканирования tar-файла с образом.

1. Берем соответствующий Docker файл и скрипты из репозитория https://github.com/Swordfish-Security/docker_cicd/tree/master/Dockerfile.
2. Запускаем его на сборку:
docker build -t dscan:image -f docker_security.df .

3. После окончания сборки создаем контейнер из образа. При этом передаём переменную окружения DOCKERIMAGE с названием интересующего нас образа и монтируем Dockerfile, который хотим анализировать, с нашей машины на файл /Dockerfile (обратите внимание, что требуется абсолютный путь до этого файла):
docker run --rm -v $(pwd)/results:/results -v $(pwd)/docker_security.df:/Dockerfile -e DOCKERIMAGE="bkimminich/juice-shop" dscan:image


[+] Setting environment variables
[+] Running Hadolint
/Dockerfile:3 DL3006 Always tag the version of an image explicitly
[+] Running Dockle
WARN    - DKL-DI-0006: Avoid latest tag
        * Avoid 'latest' tag
INFO    - CIS-DI-0005: Enable Content trust for Docker
        * export DOCKER_CONTENT_TRUST=1 before docker pull/build
INFO    - CIS-DI-0006: Add HEALTHCHECK instruction to the container image
        * not found HEALTHCHECK statement
INFO    - DKL-LI-0003: Only put necessary files
        * unnecessary file : juice-shop/node_modules/sqlite3/Dockerfile
        * unnecessary file : juice-shop/node_modules/sqlite3/tools/docker/architecture/linux-arm64/Dockerfile
        * unnecessary file : juice-shop/node_modules/sqlite3/tools/docker/architecture/linux-arm/Dockerfile
[+] Running Trivy
...
juice-shop/package-lock.json
============================
Total: 20 (UNKNOWN: 0, LOW: 1, MEDIUM: 6, HIGH: 8, CRITICAL: 5)
...
[+] Making the output look pretty
[+] Starting the main module ============================================================
[+] Converting JSON results
[+] Writing results HTML
[+] Clean exit ============================================================
[+] Everything is done. Find the resulting HTML report in results.html

Результаты


Мы рассмотрели лишь один базовый набор утилит для сканирования артефактов Docker, который, на мой взгляд, весьма эффективно покрывает приличную часть требований к безопасности образов. Существует еще большое количество платных и бесплатных инструментов, которые могут выполнять те же проверки, рисовать красивые отчеты или работать чисто в консольном режиме, охватывать системы управления контейнерами и т. д. Обзор этих инструментов и способов их интеграции, возможно, появится чуть позже.

Положительной стороной набора инструментов, который описан в статье, является то, что все они построены на открытом исходном коде и вы можете экспериментировать с ними и другими подобными инструментами, чтобы найти что именно подходит под ваши требования и особенности инфраструктуры. Безусловно все уязвимости, которые будут найдены, должны быть изучены на применимость в конкретных условиях, но это тема для будущей большой статьи.

Надеюсь, данная инструкция, скрипты и утилиты помогут вам и станут отправной точкой для создания более безопасной инфраструктуры в области, касающей контейнеризации.