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

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

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

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

  • сопутствующей инфраструктуры (ОС и платформа);

  • встроенных в них программных компонент;

  • конфигурации в рантайме.

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

Именно поэтому мы подготовили рекомендации по сборке и запуску Docker-контейнеров.

Скачать шпаргалку по безопасности Docker

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

Сборка образа

Проверьте свои образы

Внимательно выбирайте базовый образ, когда выполняете docker pull image:tag

Лучше всегда использовать проверенный образ, предпочтительно из Docker Official Images, чтобы смягчить атаки на цепочки поставок (supply-chain attacks).

В качестве базового дистрибутива рекомендуется выбирать Alpine Linux, поскольку это один из самых легких доступных дистрибутивов, что обеспечивает уменьшение поверхности атаки.

Что лучше: использовать версию с фиксированным тегом или latest?

Необходимо понимать, что теги Docker работают от менее специфичных к более специфичным, например:

python:3.9.6-alpine3.14

python:3.9.6-alpine

python:3.9-alpine

python:alpine

Все эти теги относятся к одному и тому же образу (на момент написания статьи).

Указывая более конкретную версию и фиксируя ее, вы защищаете себя от любых будущих критических изменений (breaking change). С другой стороны, использование последней версии гарантирует исправление большего количества уязвимостей. Это компромисс. Хорошая практика — привязка к стабильной версии.

Учитывая это, мы бы выбрали python:3.9-alpine.

Примечание: то же самое относится к пакетам, устанавливаемым в процессе сборки вашего образа.

Всегда используйте непривилегированного пользователя

По умолчанию процесс внутри контейнера запускается от root (id = 0).

Для реализации принципа наименьших привилегий вы должны настроить пользователя. Сделать это можно двумя способами:

  • Указать с помощью параметра -u произвольный ID пользователя, которого нет в запущенном контейнере:

docker run -u 4000 <image>

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

  • Или заранее создать пользователя в Dockerfile:

FROM <базовый образ>
RUN addgroup -S appgroup </span>
 && adduser -S appuser -G appgroup 

USER appuser
... <продолжение Dockerfile> ...

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

Используйте отдельный User ID namespace

По умолчанию демон Docker использует User namespace хоста. Следовательно, любое успешное повышение привилегий внутри контейнера будет также означать получение root-доступа как к хосту, так и к другим контейнерам.

Чтобы снизить этот риск, необходимо настроить хост и демон Docker на использование отдельного пространства имен с помощью параметра --userns-remap. Подробнее здесь.

Внимательно обращайтесь с переменными окружения

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

ENV $VAR
RUN unset $VAR

Вы ошибаетесь! $VAR все еще будет присутствовать в контейнере и может быть получен в любое время!

Чтобы предотвратить доступ к переменной в рантайме, используйте одну команду RUN для установки и очистки переменной в одном слое (не забывайте, что переменную все еще можно извлечь из образа).

RUN export ADMIN_USER="admin" \
    && ... \
    && unset ADMIN_USER

Лучше используйте директиву ARG (значения ARG недоступны после создания образа).

К сожалению, секреты слишком часто жёстко закодированы в слоях Docker-образов, поэтому для их поиска мы разработали инструмент сканирования, использующий движок секретов GitGuardian:

ggshield scan docker <image>

Подробнее о сканировании образов на наличие уязвимостей в другой статье.

Не предоставляйте доступ к сокету демона Docker

Если вы не уверены абсолютно в том, что делаете, никогда не открывайте UNIX-сокет, который слушает Docker: /var/run/docker.sock

Это основная точка входа для Docker API. Предоставление доступа к нему равносильно предоставлению неограниченного root-доступа к вашему хосту.

Никогда не открывайте его другим контейнерам:

-v /var/run/docker.sock://var/run/docker.sock

Привилегии, возможности (capabilities) и общие ресурсы

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

Лучше явно запретите добавление новых привилегий после создания контейнера с помощью опции --security-opt=no-new-privileges.

Во-вторых, capabilities — это механизм Linux, используемый Docker для превращения двоичной дихотомии "root / не root" в детализированную систему контроля доступа: ваши контейнеры запускаются с определенным набором capabilities по умолчанию, которые, скорее всего, вам все не нужны.

Рекомендуется не использовать capabilities по умолчанию, а удалить их все и явно указать только нужные: см. список capabilities по умолчанию.

Например, веб-серверу, вероятно, потребуется только NET_BIND_SERVICE для привязки к порту ниже 1024 (например, к порту 80).

В-третьих, не расшаривайте чувствительные части хостовой файловой системы:

  • корень (/);

  • устройства (/dev);

  • процессы (/proc);

  • виртуальные точки монтирования (/sys).

Если вам нужен доступ к устройствам хоста, будьте внимательны и выборочно включите доступ с помощью флагов [r|w|m] (чтение, запись и используйте mknod).

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

Контрольные группы (Control Groups, cgroup) — это механизм, используемый для управления доступом контейнеров к процессору, памяти и операциям ввода-вывода.

По умолчанию для контейнера выделяется отдельная cgroup, но если вы укажете параметр --cgroup-parent, то подвергните ресурсы хоста риску DoS-атаки, поскольку появляются разделяемые ресурсы между хостом и контейнером .

По той же причине рекомендуется ограничивать использование памяти и процессора с помощью параметров:

--memory=”400m”
--memory-swap=”1g”

--cpus=0.5
--restart=on-failure:5
--ulimit nofile=5
--ulimit nproc=5

Подробнее об ограничении ресурсов см. здесь

Файловая система

Запретите изменение корневой файловой системы

Контейнеры должны быть эфемерными и без состояния. Поэтому часто файловую систему можно монтировать только для чтения.

docker run --read-only <image>

Используйте временную файловую систему 

Если вам нужно только временное хранилище, то используйте соответствующий параметр.

docker run --read-only --tmpfs /tmp:rw ,noexec,nosuid <image>

Долговременное хранение данных

Для долговременного хранения данных у вас есть два варианта:

  • монтирование каталогов хоста (bind mount) с ограничением доступного пространства (--mount type=bind, o=size)

  • использование томов (volume) (--mount type=volume).

В обоих случаях, если контейнер не должен изменять данные, монтируйте его только для чтения.

docker run -v <volume-name>:/path/in/container:ro <image>

или

docker run --mount source=<volume-name>,destination=/path/in/container,readonly <image>

Сеть

Не используйте bridge-интерфейс по умолчанию docker0

docker0 — это сетевой мост, который создается автоматически и используется для изоляции сети хоста от сети контейнера.

По умолчанию контейнеры подключаются к сети docker0 и могут взаимодействовать друг с другом.

Лучше всегда отключать это поведение по умолчанию через параметр --bridge=none, и создавать отдельные сети для каждого соединения с помощью команды:

docker network create <network_name>

docker run --network=<network_name>
Простой пример сети Docker

Например, для веб-сервера, взаимодействующего с базой данных (запущенной в другом контейнере), лучше всего создать bridge-сеть WEB для маршрутизации входящего трафика с сетевого интерфейса хоста и еще один bridge — DB для связи между контейнерами веб-сервера и базы данных.

Не используйте network namespace хоста

То же самое, изолируйте сетевой интерфейс хоста: не используйте host-сеть (--net=host).

Логирование

По умолчанию уровень логирования — INFO, но вы можете указать другой с помощью параметра:

--log-level="debug"|"info"|"warn"|"error"|"fatal"

Менее известна возможность экспорта логов Docker: можно перенаправить потоки STDERR и STDOUT на внешний сервис логирования, используя параметр --log-driver=<logging_driver>

Также можно настроить двойное логирование, чтобы при использовании внешнего сервиса сохранить доступ Docker к логам. Если ваше приложение пишет логи в специальные файлы (обычно в /var/log), то их тоже можно перенаправить (см. официальную документацию).

Сканирование на уязвимости и секреты

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

Сканеры уязвимостей:

Сканирование секретов:

  • ggshield (open source, доступна бесплатная версия)

  • SecretScanner (бесплатная)


Материал подготовлен в рамках курса «DevOps практики и инструменты».

Всех желающих приглашаем на demo-занятие «Краткий обзор инструментов CICD: Gitlab CI, Docker, Ansible». На этом бесплатном вебинаре мы:
- узнаем, что такое CICD;
- проясним понятие infrastructure as code;
- познакомимся с Gitlab CI, Docker, Ansible;
- разберём пример совместного использования Gitlab CI, Docker, Ansible.
>> РЕГИСТРАЦИЯ