Привет, Хабр! Меня зовут Эллада, я специалист по информационной безопасности в Selectel. Продолжаю рассказывать о безопасности в Docker. В новой статье поговорим о сетевом взаимодействии контейнеров, правильном управлении привилегиями и ограничении потребления системных ресурсов.

Поделюсь, почему не стоит использовать bridge docker0 и network namespace хоста, чего не стоит делать при монтировании каталогов и многими другими советами. Придерживайтесь наших рекомендаций и сделайте работу с Docker еще более защищенной!

Дисклеймер: перед прочтением рекомендую посмотреть первую и вторую части.

Используйте навигацию, если не хотите читать текст полностью:

Сетевое взаимодействие контейнеров
Привилегии контейнеров
Монтирование каталогов
Профили безопасности
Ограничение потребления системных ресурсов
Заключение

Сетевое взаимодействие контейнеров


Контейнеры не были бы таким удобным инструментом, если не могли бы взаимодействовать друг с другом и внешними ресурсами. Поэтому стоит уделить внимание сетевым настройкам.

В этом разделе мы не будем сильно погружаться в детали организации сети в Docker — об этом можно почитать в документации. Рассмотрим некоторые рекомендации для безопасности на уровне сети.

Не используйте bridge docker0


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

Драйвер bridge docker0 используется по умолчанию, если не указано ничего другого. Он позволяет беспрепятственно общаться контейнерам внутри выделенной сети, подключенным к одному и тому же мосту. А также обеспечивает изоляцию от внешних контейнеров, которые не подключены к такому же мосту на одном хосте демона Docker.

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

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

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

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


Аналогично user namespace, необходимо ограничивать и network namespace, чтобы не использовать лишний раз сеть хоста. Здесь та же идея, что и с docker 0: нужно изолировать контейнер от хоста и других контейнеров, иначе он может получить доступ к сетевым интерфейсам и ресурсам хоста. Более того — он будет способен открывать порты с номером меньше 1024.

Традиционно в системе Linux для привязки порта процесс должен быть запущен пользователем root, иметь setUID root или CAP_NET_BIND_SERVICE. Поэтому для работы может понадобиться привилегированный доступ. Кроме того, контейнер может совершать неожиданные действия — например, завершать работу хоста.

Что из этого следует? Все просто: не используйте опцию --network=host.

Рассмотрим пример: запустим контейнер с длительной операцией sleep. Оказывается, этот процесс виден из другого контейнера, запущенного с опцией --pid=host:

dockerenjoyer@ubuntu:~$ docker run --name sleep --rm -d alpine sleep 1000 
dockerenjoyer@ubuntu:~$ docker run --pid=host --name alpine --rm -it alpine sh 
# ps | grep sleep
390431 root          0:00 sleep 1000
390655 root          0:00 grep sleep

Больше всего удивляет, что команда kill -9 из второго контейнера позволяет прервать выполнение процесса sleep в первом! Посмотрите сами:



 /# kill -9 390431
/ # ps | grep sleep
390708 root          0:00 grep sleep

Публикуйте порты только для хоста


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

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

Рекомендую при запуске контейнера привязать порты к конкретным интерфейсам на хосте. Например, вот так:

docker run -d  -p 192.168.100.4:48153:80 nginx

Таким образом, только хост с локальным адресом 192.168.100.4 будет иметь доступ к опубликованному порту контейнера.

Есть и другой вариант. Можно настроить default binding address для публикации портов, чтобы они были доступны только хосту по умолчанию. Для этого вы можете указать адрес loopback (127.0.0.1), настроив параметр ip в конфигурационном файле daemon.json.

Привилегии контейнеров


Не используйте флаг --privileged


Избегайте работы с привилегированными контейнерами с флагом --privileged. Он предоставляет контейнеру практически все возможные права и противоречит опции --cap-drop=ALL.

Почему это опасно — рассмотрим на примере. Попробуем запустить контейнер с флагом --privileged и повторим действия:

# docker run -it --privileged -p 127.0.0.1:3306:3306  --name mariadb -e MARIADB_ROOT_PASSWORD=superpass mariadb sh
9f58ac625a705cb56e25050bf67b0b631f5fd9b865bb5d7bae3aee30d50011a2
# whoami
root
# cd /root
# ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
        link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
26: eth0@if27: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
        link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
# ip link delete eth0
# ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
        link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

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

Теперь попробуем запустить контейнер от имени обычного пользователя. Посмотрим в уже работающем контейнере, какие там есть пользователи. Видим пользователя по умолчанию с id 999 — от него и запустим.

# cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
mysql:x:999:999::/home/mysql:/bin/sh

Видим, что обычный пользователь не может перейти в каталог root и удалить интерфейсы:

# docker run -u 999 -p 127.0.0.1:3306:3306  --name mariadb -e MARIADB_ROOT_PASSWORD=superpass -d mariadb
f726199bbffd090dc916d15f432835c79a6c587850f423c866174379c17738e0
# docker exec -ti mariadb sh
$ whoami
mysql
$ cd /root/
sh: 2: cd: can't cd to /root/
$ ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
        link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
28: eth0@if29: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
        link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
$ ip link delete eth0
RTNETLINK answers: Operation not permitted

Запретите новые привилегии


Рекомендую ограничить добавление новых привилегий после того, как контейнер был создан. Это можно сделать с помощью команды --security-opt=no-new-privileges:

docker run -it --security-opt=no-new-privileges:true imagename

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

Сбросьте привилегии по умолчанию


Рекомендую отказаться от всех capabilities по умолчанию и добавлять их по отдельности. Например, если веб-серверу нужна привязка к порту меньше 1024, можно включить NET_BIND_SERVICE. Полный список привилегий доступен в документации.

Вот шаблон, по которому добавляются все привилегии:

docker run --cap-drop=all --cap-add=<привилегия_1> --cap-add=<привилегия_2> <образ> …

Монтирование каталогов


С помощью опции -v можно примонтировать каталог хоста к контейнеру. Пространство имен последнего «расширяется», но какой ценой с точки зрения безопасности… Рассмотрим основные рекомендации, как делать не стоит.

  • Не монтируйте полностью файловую систему root. Это крайне опасно с точки зрения безопасности.
  • Не монтируйте /etc. Он позволяет модифицировать /etc/passwd хоста изнутри контейнера, а еще может нарушить работу заданий cron, init и systemd.
  • Без необходимости не монтируйте /bin и подобные ему каталоги (/usr/bin или /usr/sbin). Они позволят контейнеру записывать исполняемые файлы в каталог хоста и даже перезаписывать уже существующие.
  • Не монтируйте каталоги журналов хоста к контейнеру. В противном случае злоумышленник сможет изменять журналы и ликвидировать следы своих действий.
  • Не монтируйте такие чувствительные каталоги как /dev, /proc /sys, чтобы избежать несанкционированного доступа к устройствам, процессам и другим системным ресурсам.
  • Будьте внимательны к тому, какие права доступа предоставляете на смонтированный каталог. Ограничьтесь только теми, что вам необходимы, например, только чтением

Профили безопасности


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

Seccomp (Secure Computing mode, безопасный режим вычислений) — это механизм ограничения набора системных вызовов, разрешенных приложению. У каждого процесса может быть свой профиль, который представляет из себя «белый список». Он также позволяет ограничить действия, доступные в контейнере, и по умолчанию включен для Docker. Стандартный профиль Seccomp обеспечивает адекватный режим работы контейнеров и отключает около 44 системных вызовов из более чем 300.

Когда вы запускаете контейнер, активируется профиль по умолчанию, который вы можете его переопределить в --security-opt. Подробнее о профиле Seccomp в Docker можно прочитать в документации.

AppArmor (Application Armor, броня приложения) — один из нескольких модулей безопасности Linux (Linux security modules, LSM), которые можно включить в ядре. В AppArmor профиль можно связать с исполняемым файлом и на языке привилегий описать, что для него доступно, а что — нет.

Аналогично Seccomp, Docker использует профиль по умолчанию при запуске контейнеров, но его также можно переопределить в параметре --security-opt. Подробнее о работе с AppArmor можно прочитать в документации.

SELinux (Security-Enhanced Linux, Linux с улучшенной безопасностью) — еще один тип LSM. Позволяет ограничивать доступные действия с файлами и другими процессами.

SELinux управляет доступом на основе меток на процессах и объектах системы, что типично для MAC. Взаимодействие между политикой SELinux и Docker сосредоточено на двух проблемах: защите хоста и контейнеров друг от друга. В Docker по умолчанию установлен флаг -selinux-enabled.

Все упомянутые механизмы безопасности предназначены для низкоуровневого управления поведением процессов. Генерация профилей — непростая задача, ведь даже незначительное изменение приложения может превратиться в «путешествие на 20 минут». Если у вас нет возможности этим заниматься самостоятельно, то стандартных Seccomp и AppArmor для Docker будет достаточно.

Ограничение потребления системных ресурсов


По умолчанию у Docker-контейнеров нет никаких ограничений по системным ресурсам. Они могут использовать столько, сколько позволяет планировщик ядра хоста. Но есть способы, контролировать объем памяти или CPU для контейнеров.

СGroup, или контрольные группы, — это механизмы Linux, на которых также базируется контейнеризация, как и на пространствах имен. Они позволяют контролировать доступ к таким ресурсам, как CPU, ОЗУ, и дискам I/O для каждого контейнера.

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

Рекомендую ограничивать количество ресурсов при запуске контейнера. Например, вот так:

--memory=”400m”
--memory-swap=”1g”
--cpus=0.5
--restart=on-failure:5
--ulimit nofile=5
--ulimit nproc=5

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

Заключение


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

Комментарии (0)