Сегодня 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)


  1. dolfinus
    27.12.2022 11:42
    +2

    Отдельно стоит выделить, что при использовании Docker-compose контейнеры подключаются к единой создаваемой сети, и все контейнеры доступны друг для друга.

    Да, но в пределах текущего проекта. Разные проекты не имеют доступа друг к другу, если явно это не разрешить через подключение к external network.


  1. aegoroff
    27.12.2022 13:21

    Еще стоило бы упомянуть об использовании distroless образов, например отсюда https://github.com/GoogleContainerTools/distroless где в контейнере нет вообще даже шелла, только библиотеки и ваш исполняемый файл. Даже если злоумышленнику удастся проэксплуатировать уязвимость в вашем коде приводящую к исполнению шелл кода, - он не выполнится, т.к. банально нет шелла! Думаю образ gcr.io/distroless/cc-debian11 подойдет большинству приложений


    1. ggo
      28.12.2022 10:11
      +1

      строго говоря distroless (и ограниченного пользователя) недостаточно.

      нужно еще правильные права на каталоги и файлы выставлять.

      зы
      еще беда, в том числе и моего комментария, в том что сообщается о том что делаем, но не сообщается о том зачем делаем

      ;))


      1. aegoroff
        28.12.2022 11:49
        +1

        Так вроде никто и не говорит что достаточно - в безопасности вообще не бывает универсальных рецептов и задача заключается в максимальном сокращении области поражения.


  1. Ivanhoe
    27.12.2022 21:18

    Метод 6: использовать gVisor в качестве рантайма, чтобы существенно сократить возможность побега из контейнера через уязвимости ядра.


  1. raamid
    28.12.2022 16:05
    +1

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

    Что-то здесь не то. Если такое возможно, то это уже уязвимость самого Docker & containerd.


    1. aegoroff
      29.12.2022 09:05

      Нормальное явление, увы - вот например тут об этом подробнее https://habr.com/ru/company/first/blog/650553/


      1. raamid
        29.12.2022 15:41
        +1

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

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


        1. aegoroff
          29.12.2022 22:52

          На самом деле (насколько я понимаю) запуск с ключом --privileged под рутом, не то же самое что просто запуск контейнера под рутом. Последнее делают очень часто, а вот запуск с упомянутым ключом достаточно редко. Но лучше конечно когда контейнер запускается под рутом и с минимально достаточным набором прав.


  1. askharitonov
    29.12.2022 19:06

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

    Так, стоп. Вроде же root в контейнере не равен root хост-машины (в конфигурации по умолчанию)? И выйти из контейнера может позволить не уязвимость в приложении, которое там работает, а уязвимость в ядре системы, выполняющейся на хост-машине?