Контейнеры Docker уже довольно давно стали неотъемлемой частью инструментария разработчика, позволяя собирать, распространять и развертывать приложения стандартизированным способом.
Неудивительно, что при такой популярности наблюдается всплеск проблем безопасности, связанных с контейнеризацией. Также контейнеры представляют собой стандартизированную поверхность атаки: злоумышленники могут легко эксплуатировать ошибки в конфигурации и проникать из контейнера в хост-машину.
Кроме того, термин "контейнер" часто понимается неправильно: многие разработчики склонны ассоциировать концепцию изоляции с ложным чувством безопасности, полагая, что эта технология безопасна по своей сути.
Ключевым моментом здесь является то, что конфигурация контейнеров по умолчанию не ориентирована на безопасность. Их защищенность полностью зависит от:
Именно поэтому мы подготовили рекомендации по сборке и запуску Docker-контейнеров.
Скачать шпаргалку по безопасности Docker
Примечание: в управляемой среде, такой как Kubernetes, большинство этих параметров могут быть переопределены контекстом безопасности или другими правилами безопасности более высокого уровня. Подробнее здесь.
Лучше всегда использовать проверенный образ, предпочтительно из 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.
Для реализации принципа наименьших привилегий вы должны настроить пользователя. Сделать это можно двумя способами:
RUN addgroup -S appgroup </span>
&& adduser -S appuser -G appgroup
USER appuser
... <продолжение Dockerfile> ...
Чтобы снизить этот риск, необходимо настроить хост и демон Docker на использование отдельного пространства имен с помощью параметра --userns-remap. Подробнее здесь.
ENV $VAR
RUN unset $VAR
Вы ошибаетесь! $VAR все еще будет присутствовать в контейнере и может быть получен в любое время!
Чтобы предотвратить доступ к переменной в рантайме, используйте одну команду RUN для установки и очистки переменной в одном слое (не забывайте, что переменную все еще можно извлечь из образа).
RUN export ADMIN_USER="admin" \
&& ... \
&& unset ADMIN_USER
Лучше используйте директиву ARG (значения ARG недоступны после создания образа).
К сожалению, секреты слишком часто жёстко закодированы в слоях Docker-образов, поэтому для их поиска мы разработали инструмент сканирования, использующий движок секретов GitGuardian:
ggshield scan docker <image>
Подробнее о сканировании образов на наличие уязвимостей в другой статье.
Это основная точка входа для Docker API. Предоставление доступа к нему равносильно предоставлению неограниченного root-доступа к вашему хосту.
Никогда не открывайте его другим контейнерам:
-v /var/run/docker.sock://var/run/docker.sock
Лучше явно запретите добавление новых привилегий после создания контейнера с помощью опции --security-opt=no-new-privileges.
Во-вторых, capabilities — это механизм Linux, используемый Docker для превращения двоичной дихотомии "root / не root" в детализированную систему контроля доступа: ваши контейнеры запускаются с определенным набором capabilities по умолчанию, которые, скорее всего, вам все не нужны.
Рекомендуется не использовать capabilities по умолчанию, а удалить их все и явно указать только нужные: см. список capabilities по умолчанию.
Например, веб-серверу, вероятно, потребуется только NET_BIND_SERVICE для привязки к порту ниже 1024 (например, к порту 80).
В-третьих, не расшаривайте чувствительные части хостовой файловой системы:
По умолчанию для контейнера выделяется отдельная 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>
docker run -v <volume-name>:/path/in/container:ro <image>
или
docker run --mount source=<volume-name>,destination=/path/in/container,readonly <image>
По умолчанию контейнеры подключаются к сети docker0 и могут взаимодействовать друг с другом.
Лучше всегда отключать это поведение по умолчанию через параметр --bridge=none, и создавать отдельные сети для каждого соединения с помощью команды:
docker network create <network_name>
docker run --network=<network_name>
Простой пример сети Docker
Например, для веб-сервера, взаимодействующего с базой данных (запущенной в другом контейнере), лучше всего создать bridge-сеть WEB для маршрутизации входящего трафика с сетевого интерфейса хоста и еще один bridge — DB для связи между контейнерами веб-сервера и базы данных.
--log-level="debug"|"info"|"warn"|"error"|"fatal"
Менее известна возможность экспорта логов Docker: можно перенаправить потоки STDERR и STDOUT на внешний сервис логирования, используя параметр --log-driver=<logging_driver>
Также можно настроить двойное логирование, чтобы при использовании внешнего сервиса сохранить доступ Docker к логам. Если ваше приложение пишет логи в специальные файлы (обычно в /var/log), то их тоже можно перенаправить (см. официальную документацию).
Сканеры уязвимостей:
habr.com
Неудивительно, что при такой популярности наблюдается всплеск проблем безопасности, связанных с контейнеризацией. Также контейнеры представляют собой стандартизированную поверхность атаки: злоумышленники могут легко эксплуатировать ошибки в конфигурации и проникать из контейнера в хост-машину.
Кроме того, термин "контейнер" часто понимается неправильно: многие разработчики склонны ассоциировать концепцию изоляции с ложным чувством безопасности, полагая, что эта технология безопасна по своей сути.
Ключевым моментом здесь является то, что конфигурация контейнеров по умолчанию не ориентирована на безопасность. Их защищенность полностью зависит от:
- сопутствующей инфраструктуры (ОС и платформа);
- встроенных в них программных компонент;
- конфигурации в рантайме.
Именно поэтому мы подготовили рекомендации по сборке и запуску 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 пользователя, которого нет в запущенном контейнере:
Примечание: если впоследствии вам понадобится смонтировать файловую систему, то для получения доступа к файлам вам потребуется сопоставить используемый ID пользователя с пользователем хоста.
- Или заранее создать пользователя в Dockerfile:
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).
Используйте контрольные группы для ограничения доступа к ресурсам
Контрольные группы (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>

Например, для веб-сервера, взаимодействующего с базой данных (запущенной в другом контейнере), лучше всего создать 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), то их тоже можно перенаправить (см. официальную документацию).
Сканирование на уязвимости и секреты
И последнее, но не менее важное: надеюсь, теперь стало понятно, что ваши контейнеры настолько безопасны, насколько безопасно выполняемое в них программное обеспечение. Для проверки на уязвимости есть много разных инструментов как платных, так и бесплатных.Сканеры уязвимостей:
- Бесплатные:
- Коммерческие:
- Snyk (open source, доступна бесплатная версия)
- Anchore (open source, доступна бесплатная версия)
- JFrog XRay
- Qualys
- ggshield (open source, доступна бесплатная версия)
- SecretScanner (бесплатная)

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