В этой статье мы рассмотрим небезопасные варианты написания собственного Dockerfile, а также передовой опыт, включая работу с секретами и встраивание инструментов статического анализа. Однако для написания безопасного Dockerfile недостаточно иметь документ с лучшими практическими рекомендациями. Прежде всего, вам нужно организовать культуру программирования. Это включает, например, формализацию и контроль процесса использования сторонних компонентов, организацию вашей собственной спецификации программного обеспечения (SBOM), построение принципов для написания ваших собственных базовых образов, последовательное использование безопасных функций и т. д. В этом случае модель оценки зрелости BSIMM может служить отправной точкой для организации процессов. Однако в этой статье речь пойдет о технических аспектах.
Безопасное написание Dockerfile
Не использовать тег latest
Настоятельно не рекомендуется использовать тег latest
для базовых образов, поскольку он создает неопределенное поведение при обновлении базового образа. Более того, это не гарантирует, что последние версии базовых образов не будут уязвимы. Наиболее предпочтительный вариант можно увидеть в примере ниже:
FROM redis@sha256:3479bbcab384fa343b52743b933661335448f816
LABEL version 1.0
LABEL description "Test image for labels"
Также рекомендуется использовать оператор LABEL для описания вашего собственного образа, чтобы избежать ошибок со стороны тех, кто будет использовать ваш образ в будущем. Также рекомендуется определить LABEL securitytxt.
LABEL securitytxt="https://www.example.com/.well-known/security.txt"
Используйте ссылку в ярлыке security.txt
, чтобы предоставить контактную информацию для исследователей, которые могли обнаружить проблему безопасности в образе. Это особенно актуально, если образ находится в открытом доступе. Более подробную информацию по этой ссылке можно найти здесь.
Не прибегать к автоматическому обновлению компонентов
При использовании apt-get upgrade, yum update может привести к установке ранее неизвестного программного обеспечения или уязвимой версии внутри вашего контейнера. Чтобы этого избежать, устанавливайте пакеты, в которых четко указана версия каждого из них. Каждая версия должна быть проверена на наличие уязвимостей, прежде чем компонент окажется внутри контейнера. Версию компонента можно проверить с помощью инструментов класса Software Composition Analysis (SCA).
Пример установки компонента с точностью до версии:
RUN apt-get install cowsay=3.03+dfsg1-6
Если cowsay=3.03+dfsg1-6
зависит от компонента libcowsay
, то его версию тоже надо фиксировать.
Для скачивания пакетов пользуйтесь безопасным образом
Небрежное использование curl и wget позволяет злоумышленнику загружать нежелательные компоненты с неизвестных ресурсов (атака «человек посередине», при которой злоумышленник может перехватить незащищенный трафик и заменить пакет, который мы загружаем на вредоносный пакет). Это полностью разрушает концепцию нулевого доверия, согласно которой необходимо проверять любое соединение или действие перед предоставлением доступа (или, в данном случае, установкой компонента из неизвестного ресурса). В результате следующий сценарий загрузки будет грубой ошибкой, поскольку сценарий, полученный из ненадежного источника по незащищенному каналу, выполняется без надлежащих проверок:
RUN wget http://somesite.com/some-package/install.sh | sh
Чтобы убедиться, что загруженный компонент действительно соответствует нашим ожиданиям, хорошим решением может быть использование GNU Privacy Guard (GPG). Посмотрим, как это работает.
В большинстве случаев поставщики также предоставляют хеш-значение библиотеки или программного обеспечения, которое мы можем проверить при загрузке. Этот хэш подписывается поставщиками с использованием закрытого ключа в структуре GPG, а открытые ключи помещаются в репозиторий. В следующем примере показано, как может выглядеть безопасная загрузка компонентов Node.js:
RUN gpg --keyserver pool.sks-keyservers.net \
--recv-keys 7937DFD2AB06298B2293C3187D33FF9D0246406D \
114F43EE0176B71C7BC219DD50A3051F888C628D
ENV NODE_VERSION 0.10.38
ENV NPM_VERSION 2.10.0
RUN curl -SLO "http://nodejs.org/dist/v$NODE_VERSION/node-v \
$NODE_VERSION-linux-x64.tar.gz" \
&& curl -SLO "http://nodejs.org/dist/v$NODE_VERSION/\SHASUMS256.txt.asc" \
&& gpg --verify SHASUMS256.txt.asc \
&& grep " node-v$NODE_VERSION-linux-x64.tar.gz$" SHASUMS256.txt.asc | sha256sum -c -
Давайте разберемся, что здесь происходит:
-
Получение открытых GPG-ключей
-
Скачивание Node.js пакета
-
Скачивание хеш-суммы Node.js пакета на базе алгоритма SHA256
-
Использование GPG-клиента для проверки, что хеш-сумма подписана тем, кто владеет закрытыми ключами
-
Проверяем, что вычисленная хеш-сумма от пакета совпадает со скачанной с помощью sha256sum
Если один из тестов не пройден, установка будет прервана. Таким образом, мы убедились, что компонент требуемой версии не был заменен и соответствует ожидаемой версии.
Иногда разработчикам приходится использовать сторонние репозитории для установки компонента с помощью deb или rpm. В этом случае мы также можем использовать GPG, и проверка хэша будет выполняться менеджерами пакетов во время загрузки.
Пример безопасного добавления GPG-ключей вместе с источником пакетов.
RUN apt-key adv --keyserver hkp://pgp.mit.edu:80 \
--recv-keys 573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62
RUN echo "deb http://nginx.org/packages/mainline/debian/\
jessie nginx" >> /etc/apt/sources.list
В случае, если для пакетов цифровая подпись в явном виде не предоставляется вендором, ее необходимо предварительно создать и сравнить в рамках сборки.
Пример для SHA256:
RUN curl -sSL -o redis.tar.gz \
http://download.redis.io/releases/redis-3.0.1.tar.gz \
&& echo "0e21be5d7c5e6ab6adcbed257619897db59be9e1ded7ef6fd1582d0cdb5e5bb7 \
*redis.tar.gz" | sha256sum -c -
Не использовать ADD
Инструкция ADD
, получившая в качестве параметра путь к архиву, автоматически распаковывает этот архив при выполнении. Это, в свою очередь, может привести к появлению в контейнере zip-бомбы. При распаковке zip-бомба может вызвать отказ в обслуживании приложения (DoS), заполнив все выделенное свободное пространство.
Еще одна небольшая особенность команды ADD
заключается в том, что вы можете передать ей URL-адрес в качестве параметра, и она будет извлекать контент во время сборки, что также может привести к атаке типа «человек посередине»:
ADD https://cloudberry.engineering/absolutely-trust-me.tar.gz
Как и в предыдущей рекомендации, стоит добавить компоненты в образ с помощью инструкции COPY
, поскольку она работает с локальными данными, предварительно проверив их с помощью инструментов SCA.
USER задавать в конце Dockerfile
Если злоумышленнику удастся захватить оболочку внутри вашего контейнера, он может стать пользователем root, что значительно упростит дальнейшие атаки за пределами контейнера. Чтобы избежать этого, вы должны явно указать пользователя с помощью оператора USER
. Однако это работает только в том случае, если приложение не требует прав администратора после компиляции.
RUN groupadd -r user_grp &&
useradd -r -g user_grp user
USER user
В процессе инициализации применять gosu вместо sudo
Инструмент gosu полезен, когда вам нужно предоставить привилегии root после компиляции Dockerfile во время инициализации, но приложение должно работать в непривилегированном режиме.
В следующем примере выполняется команда chown
в сценарии entrypoint-скрипта, требующем прав администратора, и приложение продолжает работать как пользователь redis.
#!/bin/bash
set -e
if [ "$1" = 'redis-server' ];
then
chown -R redis .
exec gosu redis "$@"
fi
exec "$@"
Основная цель инструмента — запуск процессов от определенного пользователя, но в отличие от sudo и su, gosu не делает fork процессов, как показано ниже:
$ docker run -it --rm ubuntu:trusty su -c 'exec ps aux'
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 46636 2688 ? Ss+ 02:22 0:00 su -c exec ps a
root 6 0.0 0.0 15576 2220 ? Rs 02:22 0:00 ps aux
$ docker run -it --rm ubuntu:trusty sudo ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 3.0 0.0 46020 3144 ? Ss+ 02:22 0:00 sudo ps aux
root 7 0.0 0.0 15576 2172 ? R+ 02:22 0:00 ps aux
$ docker run -it --rm -v $PWD/gosu-amd64:/usr/local/bin/gosu:ro ubuntu:trusty gosu root ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 7140 768 ? Rs+ 02:22 0:00 ps aux
Это сохраняет идею о том, что контейнер связан с одним процессом. Однако, по словам разработчиков, gosu не заменяет sudo, поскольку в рамках этого fork’а взаимодействие с Linux PAM осуществляется через функции pam_open_session ()
и pam_close_session ()
. Использование gosu вместо sudo вне процесса инициализации может привести к сбою в работе приложения.
Минимальные образы и Distroless images
Можно значительно уменьшить поверхность атаки злоумышленника, отказавшись от базовых образов дистрибутивов Linux (Ubuntu, Debian, Alpine) и переключившись на образы без сбоев. Это образы, которые содержат только само приложение и необходимые для него зависимости, без использования лишних системных компонентов (например, bash). Одним из очевидных преимуществ, помимо уменьшения поверхности атаки и возможностей злоумышленника, является уменьшение размера образа. Это, в свою очередь, снижает «шум» в результатах сканирования с помощью таких инструментов, как Trivy, Clair и т. д.
У этой стратегии есть много преимуществ в плане безопасности, но она очень усложняет разработчикам, которым необходимо выполнять отладку внутри контейнера. Альтернативным вариантом может быть использование минимального образа на основе, например, сборки Alpine UNIX. Если следовать этому подходу ответственно, образ разработчика будет содержать минимум зависимостей, а это означает, что сканеры образов могут генерировать меньший объем результатов, среди которых легче отфильтровать ложные срабатывания.
Цикл статей по созданию минимального образа:
Одним из вариантов, как можно эффективно сократить размер образа является multi-stage сборка, которая ко всему прочему поможет безопасно работать с секретами. Об этом будет в следующем подразделе.
Полезным инструментом также является Docker-slim, позволяющий уменьшить размер написанного образа.
Безопасная работа с секретами
Секреты могут быть ошибочно переданы в качестве параметра инструкции ENV
или переданы внутри образа в виде текстового файла. Также секреты можно скачать через wget. Такие сценарии недопустимы, потому что злоумышленник может легко получить доступ к секретам. Это можно сделать, например, обратившись к Docker API:
# docker inspect ubuntu -f {{json .Config.Env}}
["SECRET=mypassword", ...]
Кроме того, злоумышленник может получить доступ к секретам через журналы, каталог / proc
или утечку исходных файлов. В этом случае может быть лучше использовать решение класса Vault, например, HashiCorp Vault или Conjur, но мы рассмотрим другие методы.
Многоэтапные сборки
Многоэтапная сборка (multi-stage) не только поможет вам уменьшить размер вашего образа, но и организовать эффективное управление секретами. Основной принцип — извлекать и управлять секретами на промежуточном этапе создания образа, которые позже удаляются. В результате конфиденциальные данные не будут включены в окончательную сборку образа.
#builder
FROM ubuntu as intermediate
WORKDIR /app
COPY secret/key /tmp/
RUN scp -i /tmp/key build@acme/files .
#runner
FROM ubuntu
WORKDIR /app
COPY --from=intermediate /app .
Один из минусов этого сценария — сложное кэширование, что ведет к замедлению сборки.
BuildKit
Начиная с версии Docker 18.09, появляется экспериментальная интеграция с сборщиком BuildKit, который помимо повышения производительности будет эффективно работать с секретами.
Пример синтаксиса:
# syntax = docker/dockerfile:1.0-experimental
FROM alpine
# shows secret from default secret location
RUN --mount=type=secret,id=mysecret cat /run/secrets/mysecre
# shows secret from custom secret location
RUN --mount=type=secret,id=mysecret,dst=/foobar cat /foobar
После выполнения сборки с помощью buildkit с указанием ключа --secret
секретная информация не сохранится в конечном образе.
Пример:
$ docker build --no-cache --progress=plain --secret id=mysecret,src=mysecret.txt .
Подробнее можно прочитать в официальной документации Docker.
С опаской относиться к рекурсивному копированию
Команда в примере ниже может привести к тому, что секретный файл случайно окажется внутри вашего образа, если он находится в том же каталоге. Если у вас есть такие файлы, не забудьте использовать .dockerignore
. Файлы в образе могут включать .git, .aws, .env.
COPY . .
Одним из примеров таких ошибок является утечка исходного кода Twitter Vine. Итак, в 2016 году специалист по информационной безопасности обнаружил на DockerHub образ vinewww, который обнаружил исходный код Vine, ключи API и секреты сторонних сервисов.
Анализаторы Dockerfile
Используйте анализаторы Dockerfile, которые можно встроить в ваш пайплайн сборки. Это могут быть:
Hadolint — это простой линтер Dockerfile. Большинство проверок не связаны с безопасностью и основаны на официальных рекомендациях Docker (ссылка). Однако для наиболее частых ошибок такой проверки будет достаточно.
Conftest — это анализатор конфигурационных файлов, в том числе Dockerfile. Проверка Dockerfile требует предварительной записи правил в Rego, которые также используются в быстрорастущей технологии Open Policy Agent для защиты облачных сред во время работы. Это позволит сохранить вариативность, возможность настройки и не требует изучения ранее неизвестных языков. Conftest не имеет встроенного набора правил, поэтому предполагается, что вы напишете их сами. Вы можете использовать эту статью как отправную точку.
Практика
Вы можете попробовать использовать распространенные уязвимости, написав файл Docker с помощью образа Pentest-in-Docker. Пошаговое описание можно найти в репозитории. Одна из основных ошибок заключается в использовании debian: wheazy
, старого образа Debian, который поддерживает ненужное приложение Bash, содержащее уязвимость удаленного выполнения кода (RCE). Таким образом, злоумышленник может получить доступ к учетной записи службы www-data, отправив запрос на соединение в обратную оболочку. Вторая ошибка — использование sudo, которое позволяет злоумышленнику повысить привилегии с www-данных до root внутри контейнера. И, наконец, отсутствие USER
в конце Dockerfile, что позволяет злоумышленнику выполнять действия от имени пользователя root, если он получает доступ к Docker API.
Заключение
Часто вы начинаете проекты с общего образа контейнера Docker, например, как обычно, пишете Dockerfile с узлом FROM. Однако при указании образа узла вы должны помнить, что полностью установленный дистрибутив Debian является базовым образом, используемым для его сборки. Если вашему проекту не требуются общие системные библиотеки или системные утилиты, лучше избегать использования полнофункциональной операционной системы в качестве базового образа.