Docker: 10 Security Best Practices

Всем привет!

Сегодня мы представим перевод статьи компании Snyk Ltd. авторами которой являются 2 специалиста:

  • @liran_tal – Node.js Security WG & Developer Advocate из компании Snyk
  • @omerlh – DevSecOps Engineer из компании Soluto
Скачать памятку по безопасности образов Docker

Памятка рассчитана для повышения безопасности контейнеров Docker.

  1. Используйте минимальные образы

Зачастую Вы начинаете проекты с общего образа контейнера Docker, например, писать Dockerfile с FROM node , как обычно. Однако при указании образа ноды вы должны учитывать, что полностью установленный дистрибутив Debian является базовым образом, который используется для его сборки. Если вашему проекту не требуются какие-либо общие системные библиотеки или системные утилиты, лучше избегать использования полнофункциональной операционной системы в качестве базового образа.

В отчете Snyk о состоянии безопасности открытого исходного кода – 2019 г. мы обнаружили, что многие популярные контейнеры Docker, представленные на Docker Hub, объединяют в себе образы, содержащие множество известных уязвимостей. Например, когда вы используете популярный образ ноды, такой как Docker Pull Node, вы фактически вводите ОС в свое приложение, которая унаследует 580 уязвимостей в системных библиотеках операционной системы.

each of the top ten docker images in docker hub were found to have at least 30 vulnerabilities

2. Пользователь с наименьшими привилегиями

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

Чтобы минимизировать воздействие, включите создание выделенного пользователя и выделенной группы в образе Docker для приложения; используйте директиву USER в Dockerfile, чтобы убедиться, что контейнер запускает приложение с минимально возможным доступом. И если изначально в контейнере не предусмотрен выделенный пользователь, рекомендуется его добавить в контейнер перед использованием. Например (вместо username и usergroup укажите свои значения):

FROM ubuntu
RUN mkdir /app
RUN groupadd -r usergroup && useradd -r -s /bin/false -g usergroup username
RUN chown -R username:usergroup /app
WORKDIR /app
COPY . /app
USER username
CMD node index.js
  • Создали системного юзера, без пароля, шелла и хоума
  • Создали группу и добавили туда созданного пользователя
  • Дали права на приложение в контейнере

Если Вы фанат Node.js и образов alpine, то всё уже сделано за Вас, пользователь node уже добавлен. Пример:

FROM node:10-alpine 
RUN mkdir /app
COPY . /app
RUN chown -R node:node /app
USER node
CMD [“node”, “index.js”]

А для тех, кто занимается разработкой на Node.js есть отдельный Best Practices на Github

3. Всегда подписывай и проверяй образы для избежания MITM атак

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

Как проверять образ? Настройки Docker по умолчанию позволяют извлекать образы Docker без проверки их подлинности, что потенциально может привести к появлению произвольных образов Docker, происхождение и автор которых не проверены.

Рекомендуется всегда проверять образы перед их извлечением, независимо от политики. Чтобы поэкспериментировать с проверкой, временно включите Docker Content Trust с помощью следующей команды:

export DOCKER_CONTENT_TRUST=1

А теперь попробуйте подтянуть образ который Вы уверены не подписан – запрос будет отклонён и образ не будет загружен.

Как подписывать образы?

Предпочитайте подписанные образы Docker, полученные от проверенных партнеров, которые были проверены на Docker Hub, а не образы, происхождение и подлинность которых вы не можете проверить.

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

Когда Docker Content Trust включен, как мы показали выше, при сборке образа, Docker его подписывает. Когда образ подписывается впервые, Docker генерирует и сохраняет закрытый ключ в ~/docker/trust для вашего пользователя. Этот закрытый ключ затем используется для подписи любых дополнительных образов по мере их создания.

Подробные инструкции по настройке подписанных образов см. В официальной документации Docker.

4. Ищи, исправляй и следи за уязвимостями в open source ПО.

Когда мы выбираем базовый образ для нашего контейнера Docker, мы косвенно берем на себя риск всех проблем безопасности, с которыми связан базовый образ. Это могут быть плохо настроенные значения по умолчанию, которые не способствуют безопасности операционной системы, а также системные библиотеки, которые связаны с выбранным нами базовым образом.

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

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

Snyk Docker Scanning and Remediation from the CLI

Просканировать образ Docker на уязвимости можно с помощью этих команд:

# fetch the image to be tested so it exists locally
$ docker pull node:10
# scan the image with snyk
$ snyk test --docker node:10 --file=path/to/Dockerfile

Snyk также позволяет мониторить появление новых уязвимостей и уведомлять Вас об этом.

Сделать это можно командой:

snyk monitor --docker node:10

На основе сканирования, выполненного пользователями Snyk, было обнаружено, что 44% сканирования образов Docker имели известные уязвимости и для которых были доступны более новые и более безопасные базовые версии образов. Этот совет по исправлению положения уникален для Snyk, в соответствии с которым разработчики могут предпринимать действия и обновлять свои образы Docker.

Snyk также обнаружил, что для 20% всех сканирований образа Docker потребуется только перестроение образа Docker, чтобы уменьшить количество уязвимостей.

5. Не оставляйте важную информацию в образах Docker.

Иногда при создании приложения внутри образа Docker вам нужны такие секреты, как закрытый ключ SSH для извлечения кода из частного репозитория или токены для установки закрытых пакетов. Если вы копируете их в промежуточный контейнер Docker, они кэшируются на том уровне, к которому они были добавлены, даже если вы удалите их позже. Эти токены и ключи должны храниться вне Dockerfile.

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

FROM: ubuntu as intermediate

WORKDIR /app
COPY secret/key /tmp/
RUN scp -i /tmp/key [email protected]/files .

FROM ubuntu
WORKDIR /app
COPY --from intermediate /app .

Используйте команду secret

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

# syntax = docker/dockerfile:1.0-experimental
FROM alpine

# shows secret from default secret location
RUN --mount=type=secret,id=mysecret cat /run/secrets/mysecret

# shows secret from custom secret location
RUN --mount=type=secret,id=mysecret,dst=/foobar cat /foobar

Остерегайтесь рекурсивной копии
Вы также должны быть внимательны при копировании файлов в создаваемый образ. Например, команда COPY . . рекурсивно копирует всю папку контекста сборки в образ Docker, что может также привести к копированию конфиденциальных файлов

Если в вашей папке есть конфиденциальные файлы (private.key; settings.json и т.д.), удалите их или используйте .dockerignore, чтобы игнорировать их.

6. Используйте фиксированные теги для неизменности

Каждый образ Docker может иметь несколько тегов, которые являются вариантами одних и тех же образов. Самый распространенный тег – latest, представляющий последнюю версию образа. Теги образов не являются неизменяемыми, и автор образа может публиковать один и тот же тег несколько раз.

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

  • Предпочитайте наиболее конкретный доступный тег. Если образ имеет несколько тегов, таких как: 8 и: 8.0.1 или даже: 8.0.1-alpine, предпочтите последнее, так как он является наиболее конкретной ссылкой на образ. Избегайте использования самых общих тегов, например, последних. Имейте в виду, что при закреплении определенного тега он может быть в конечном итоге удален.
  • Чтобы устранить проблему, связанную с тем, что определенный тег образа становится недоступным и становится ограничителем показа для групп, которые полагаются на него, рассмотрите возможность запуска локального зеркала этого образа в реестре или учетной записи, которая находится под вашим собственным контролем. Важно принять во внимание затраты на обслуживание, необходимые для этого подхода, потому что это означает, что вам нужно поддерживать реестр. Хорошей практикой является тиражирование образа, которое вы хотите использовать в вашем реестре, чтобы убедиться, что образ, который вы используете, не изменяется.
  • Будьте очень конкретны! Вместо того, чтобы тянуть метку, потяните образ, используя конкретную ссылку SHA256 на образ Docker, который гарантирует, что вы получите один и тот же образ для каждого запроса. Однако обратите внимание, что использование ссылки SHA256 может быть рискованным, если изменится образ, тот же хеш больше не сработает.

7. Используйте COPY вместо ADD

Docker предоставляет две команды для копирования файлов с хоста в образ Docker при его создании: COPY и ADD.

Инструкции похожи по своей природе, но отличаются по своей функциональности:

  • COPY – рекурсивно копирует локальные файлы с указанием явных исходных и целевых файлов или каталогов. С COPY вы должны объявить места.
  • ADD – рекурсивно копирует локальные файлы, неявно создает каталог назначения, когда он не существует, и принимает архивы как локальные или удаленные URL-адреса в качестве источника, который он расширяет или загружает соответственно в каталог назначения.

Эти различия между ADD и COPY важны. Помните об них, чтобы избежать потенциальных проблем безопасности:

Когда удаленные URL-адреса используются для загрузки данных непосредственно в исходное местоположение, они могут привести к атакам типа «человек посередине», которые изменяют содержимое загружаемого файла. Кроме того, происхождение и подлинность удаленных URL-адресов должны быть дополнительно проверены. При использовании COPY источник для файлов, которые будут загружены с удаленных URL-адресов, должен быть объявлен через безопасное соединение TLS, и их происхождение также должно быть проверено.
Использование COPY позволяет отделить добавление архива от удаленных мест и распаковать его в виде разных слоев, что оптимизирует кэш образов. Если требуются удаленные файлы, объединение их всех в одну команду RUN, которая впоследствии загружает, извлекает и очищает, оптимизирует однослойную операцию на нескольких уровнях, которая потребовалась бы при использовании ADD.
Когда используются локальные архивы, ADD автоматически извлекает их в каталог назначения. Хотя это может быть приемлемо, это добавляет риск уязвимостей Zip-бомб и уязвимостей Zip Slip, которые затем могут запускаться автоматически.

8. Используйте метки данных

Метки образов предоставляют метаданные для образа, которые вы создаете. Это помогает пользователям понять, как легче использовать образ. Самым распространенной меткой является «maintainer», которая указывает адрес электронной почты и имя лица, поддерживающего этот образ. Добавьте метаданные с помощью следующей команды LABEL:

LABEL maintainer="[email protected]"

В дополнение к вышеуказанной метке добавьте любые метаданные, которые важны для вас. Эти метаданные могут содержать: хеш коммита, ссылку на соответствующую сборку, статус качества (все ли тесты прошли?), исходный код, ссылку на местоположение файла SECURITY.TXT и т. д.

Хорошей практикой является принятие файла SECURITY.TXT (RFC5785), который указывает на вашу политику ответственного раскрытия для вашей схемы меток Docker при добавлении меток, например:

LABEL securitytxt="https://securixy.kz/.well-known/security.txt"

Подробнее о метках 

9. Используйте многоэтапную сборку для небольших и безопасных образов

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

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

Голанг является отличным примером. Чтобы построить приложение Golang, вам нужен компилятор Go. Компилятор создает исполняемый файл, который работает в любой операционной системе, без зависимостей, включая чистые образы.

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

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

10. Используйте линтер
Начните использовать линтер, чтобы избежать распространенных ошибок, и установить рекомендации по передовой практике, которым инженеры могут следовать в автоматическом режиме.

Одним из таких линтеров является hadolint. Он анализирует Dockerfile и показывает предупреждение о любых ошибках, которые не соответствуют его правилам передовой практики.

Docker: 10 Security Best Practices

Пример статического анализа кода безопасности в dockerfile lint от hadolint

Hadolint становится еще более мощным, когда используется в интегрированной среде разработки (IDE). Например, при использовании hadolint в качестве расширения VSCode при вводе появляются ошибки лининга. Это помогает в написании лучших Dockerfiles быстрее.

Скачать памятку по безопасности образов Docker

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