Привет, Хабр! Меня зовут Эллада, я специалист по информационной безопасности в 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-контейнерах. Сохраните материалы в закладки, чтобы иногда пользоваться ими как чек-листами и шпаргалками. Делитесь в комментариях, о каких аспектах вы хотите почитать в будущем.