Сегодня Docker является одним из самых популярных инструментов в области контейнеризации. В основе Docker заложена концепция использования контейнеров. С технической точки зрения контейнер — это процесс, наподобие тех, которые запускаются в операционных системах. Как и любой другой продукт, Docker подвержен различным атакам и уязвимостям. В этой статье будут рассмотрены методы, которые помогут повысить уровень безопасности контейнеров Docker.
Метод 1. Использование непривилегированных пользователей внутри контейнера
По умолчанию Docker запускает контейнеры от имени root пользователя. Чем грозит использование root пользователя в запущенных контейнерах?
Если в приложении, которое работает в контейнере, будет обнаружена уязвимость, то это может позволить злоумышленникам выйти из контейнера и выполнить различные действия с правами root на хостовой ОС. Тем самым вы создаете слишком привилегированную среду, которая дает злоумышленникам больше возможностей в случае взлома. Чтобы убедиться в этом, запустим пару разных контейнеров. И первый — образ с веб-сервером Nginx. Для этого используем команду:
docker run -d --name nginx nginx
Далее осуществим вход в оболочку контейнера при помощи команды:
docker exec -it nginx /bin/bash
После того, как мы оказались в контейнере, можно заметить, что в строке приглашения к вводу отображается пользователь root и символ #. Также проверить, под каким пользователем мы авторизованы в системе, можно при помощи команды:
id
Далее запустим контейнер с образом легковесной операционной системы Alpine:
docker run -d -it --name alpine alpine
Осуществим вход в оболочку Bourne shell (sh):
docker exec -it alpine /bin/sh
И выполним команду id:
Как и в случае с контейнером Nginx, можно увидеть, что мы подключились к контейнеру как root.
В качестве решения при запуске контейнеров всегда указывайте имя обычного (непривилегированного пользователя). Для этого при запуске контейнеров необходимо использовать ключ -u (или длинную версию --user) в котором можно задать необходимого пользователя. Рассмотрим пример с запуском контейнера с Nginx который мы запускали ранее:
docker run -d --name nginx nginx
Но теперь подключимся к созданному контейнеру при помощи exec и зададим пользователя nobody:
docker exec -it --user nobody nginx /bin/bash
Как видно на скриншоте выше, приглашение к вводу содержит имя пользователя nobody и символ $. Это означает, что сессия запущена от имени обычного непривилегированного пользователя. Также указать конкретного пользователя можно и в Dockefile. Для этого используется инструкция USER:
FROM ubuntu:16.04
RUN useradd -u 8877 nonroot
USER nonroot
Соберем образ при помощи команды:
docker build -t nonrootuser .
И запустим:
docker run -it nonrootuser /bin/bash
Выполним команду id:
Как можно заметить, мы подключились к контейнеру от имени ранее созданного пользователя — nonroot.
Метод 2. Ограничение на использование capabilities (привилегий)
Docker использует механизм ядра Linux под названием capabilities — средства для управления привилегиями в операционных системах семейства Linux. Если вкратце — это атрибуты ядра Linux, которые предоставляют привилегии root пользователя процессам или исполняемым файлам. К таким привилегиям можно отнести право на изменение UID процесса, право монтировать файловые системы, изменять конфигурации MAC и т. д. С полным списком всех доступных привилегий можно ознакомиться по ссылке.
При запуске контейнеров Docker можно как задать необходимые привилегии, так и выставить запрет на их использование.
Самым безопасным способом является запрет на использование всех привилегий, а затем выдача разрешений только для необходимых привилегий. Для этого в Docker существуют следующие опции:
--cap-drop — запрет на выполнение capabilities
--cap-add — разрешение на выполнение capabilities
Обе эти опции можно использовать сразу для одного контейнера. Рассмотрим на примере запуска контейнера с alpine:
docker run -d -it --cap-drop all --cap-add CAP_SYS_ADMIN alpine /bin/sh
В примере выше мы сначала создали запрет на выполнение любых привилегий в контейнере (опция --cap-drop со значением all), а потом разрешили выполнять привилегию CAP_SYS_ADMIN (--cap-add). Привилегия CAP_SYS_ADMIN, в частности, позволяет монтировать и размонтировать файловые системы.
Метод 3. Использование опции no-new-privileges
Использование опции no-new-privileges позволит предотвратить повышения привилегий в контейнере путем использования setuid и setgid.
setuid — флаги с правами доступа, которые разрешают пользователям запускать исполняемые файлы с правами владельца исполняемого файла.
setgid — флаги с правами доступа, которые разрешают пользователям запускать исполняемые файлы с правами группы исполняемого файла.
Ознакомиться подробнее с данными правами доступа можно по ссылкам setuid и setgid.
Перейдем к демонстрации. Соберем образ из следующего Dockerfile:
FROM ubuntu
RUN cp /bin/bash /bin/setuidbash && chmod 4755 /bin/setuidbash
RUN useradd -ms /bin/bash newuser
USER newuser
CMD ["/bin/bash"]
В собранном образе создадим еще одну оболочку bash с именем setuidbash и назначим ей права root пользователя при помощи setuid. Это означает, что созданную оболочку будем использовать для получения root прав. Соберем образ:
docker build -t newpriv .
Далее запустим контейнер:
docker run -it newpriv
Так как в Dockerfile мы указали пользователя, то сессия запустилась от имени обычного непривилегированного пользователя с именем newuser. Теперь повысим наши привилегии до root, выполнив команду:
/bin/setuidbash -p
Ключ -p (privileged) означает, что запуск будет произведен как suid. Как можно увидеть на скриншоте выше, приглашение к вводу изменилось с символа $ (обычного пользователя) на символ # (root пользователь). Это означает, что получение root прав выполнилось успешно.
Чтобы предотвратить повышение привилегий, контейнер необходимо запустить с опцией no-new-privileges:
docker run -it --security-opt=no-new-privileges:true newpriv
Теперь выполним команду /bin/setuidbash -p еще раз:
Повышений привилегий не сработало.
Метод 4. Использование файловых систем в режиме «только для чтения»
Если в контейнере нет надобности в записи и создании каких-либо файлов, то его стоит запускать в режиме «только для чтения» (read-only). При включенном режиме в контейнере нельзя будет создавать новые файлы и редактировать существующие. В плане безопасности это приведет к тому, что контейнер не сможет записывать или изменять какие-либо данные внутри себя. Проверим данный способ на практике. Для начала запустим контейнер с ОС ubuntu:
docker run -it -d --name ubuntu_shell ubuntu
Перейдем в контейнер и создадим файл test.txt:
docker exec -it ubuntu_shell /bin/bash
touch test.txt
Теперь запустим контейнер с опцией -- read-only:
docker run --read-only -it -d --name ubuntu_read_only ubuntu
Подключимся к нему и попытаемся создать файл test.txt — чтобы проверить, как сработает опция:
docker exec -it ubuntu_read_only /bin/bash
Возникла ошибка touch: cannot touch 'test.txt': Read-only file system, которая сообщает, что файловая система запущена в режиме только для чтения.
Метод 5. Не использовать сетевой интерфейс docker0
По умолчанию для всех создаваемых контейнеров Docker использует свой bridge интерфейс под названием docker0, который всегда создается в системе вместе с установкой Docker:
Отдельно стоит выделить, что при использовании Docker-compose контейнеры подключаются к единой создаваемой сети, и все контейнеры доступны друг для друга. Все контейнеры, подключенные к интерфейсу docker0 будут взаимодействовать друг с другом. Однако для каждого контейнера необходимо создавать свою отдельную сеть и подключать к этой сети только те контейнеры, которые будут использоваться. Тем самым можно изолировать нужные контейнеры или вовсе отключить их от сети. Для создания сетей в Docker используется команда:
docker network create --driver <тип_сети> <имя_сети>
Где параметр driver может принимать одно из следующих значений:
bridge;
host;
overlay;
ipvlan;
macvlan.
Чтобы подключить контейнер к сети, необходимо использовать параметр --network и в качестве значения указать имя требуемой сети.
Контейнеры Docker являются отличным вариантом для оперативной доставки программного обеспечения и позволяют существенно сократить использование как технических, так и финансовых ресурсов. Популярность Docker привлекла большое внимание со стороны злоумышленников. В этом контексте стоит уделить безопасности особое внимание.
НЛО прилетело и оставило здесь промокод для читателей нашего блога:
— 15% на все тарифы VDS (кроме тарифа Прогрев) — HABRFIRSTVDS.
Комментарии (10)
aegoroff
27.12.2022 13:21Еще стоило бы упомянуть об использовании distroless образов, например отсюда https://github.com/GoogleContainerTools/distroless где в контейнере нет вообще даже шелла, только библиотеки и ваш исполняемый файл. Даже если злоумышленнику удастся проэксплуатировать уязвимость в вашем коде приводящую к исполнению шелл кода, - он не выполнится, т.к. банально нет шелла! Думаю образ gcr.io/distroless/cc-debian11 подойдет большинству приложений
ggo
28.12.2022 10:11+1строго говоря distroless (и ограниченного пользователя) недостаточно.
нужно еще правильные права на каталоги и файлы выставлять.
зы
еще беда, в том числе и моего комментария, в том что сообщается о том что делаем, но не сообщается о том зачем делаем;))
aegoroff
28.12.2022 11:49+1Так вроде никто и не говорит что достаточно - в безопасности вообще не бывает универсальных рецептов и задача заключается в максимальном сокращении области поражения.
Ivanhoe
27.12.2022 21:18Метод 6: использовать gVisor в качестве рантайма, чтобы существенно сократить возможность побега из контейнера через уязвимости ядра.
raamid
28.12.2022 16:05+1Если в приложении, которое работает в контейнере, будет обнаружена уязвимость, то это может позволить злоумышленникам выйти из контейнера и выполнить различные действия с правами root на хостовой ОС.
Что-то здесь не то. Если такое возможно, то это уже уязвимость самого Docker & containerd.
aegoroff
29.12.2022 09:05Нормальное явление, увы - вот например тут об этом подробнее https://habr.com/ru/company/first/blog/650553/
raamid
29.12.2022 15:41+1Спасибо за статью, не знал. Для тех кому лень кликать по ссылке - речь идет о запуске контейнеров с правами администратора. О ужас, если запустить контейнер с правами админа, то он получит права админа.
Я думаю, что те кто это делает отлично понимают что делают и для них докер служит не средством изоляции а инструментом для удобного развертывания. Более того, благодаря докеру можно распределить софт между привилегированными контейнерами и обычными, чтобы обеспечить баланс безопасности и функционала или что там нужно для привилегированных контейнеров.
aegoroff
29.12.2022 22:52На самом деле (насколько я понимаю) запуск с ключом --privileged под рутом, не то же самое что просто запуск контейнера под рутом. Последнее делают очень часто, а вот запуск с упомянутым ключом достаточно редко. Но лучше конечно когда контейнер запускается под рутом и с минимально достаточным набором прав.
askharitonov
29.12.2022 19:06Если в приложении, которое работает в контейнере, будет обнаружена
уязвимость, то это может позволить злоумышленникам выйти из контейнера и
выполнить различные действия с правами root на хостовой ОС.Так, стоп. Вроде же root в контейнере не равен root хост-машины (в конфигурации по умолчанию)? И выйти из контейнера может позволить не уязвимость в приложении, которое там работает, а уязвимость в ядре системы, выполняющейся на хост-машине?
dolfinus
Да, но в пределах текущего проекта. Разные проекты не имеют доступа друг к другу, если явно это не разрешить через подключение к external network.