![](https://habrastorage.org/webt/8w/2q/ga/8w2qgad0hpcszydr-apdn1uib-8.png)
Docker контейнеры — самая популярная технология для контейнеризации. Изначально она использовалась в основном для dev и test окружений, со временем перешла и в production. Docker контейнеры начали плодиться в production среде, как грибы после дождя, однако мало из тех, кто использует данную технологию, задумывался о том, как же безопасно публиковать Docker контейнеры.
Основываясь на OWASP, мы подготовили список правил, выполнение которых позволит значительно обезопасить ваше окружение, построенное на Docker контейнерах.
Правило 0
Хостовая машина и Docker должны содержать все актуальные обновления
Для защиты от известных уязвимостей, приводящих к выходу за пределы окружения контейнера в хостовую систему, которые, как правило, заканчиваются повышением привилегий на хостовой системе, крайне важна установка всех патчей для хостовой ОС, Docker Engine и Docker Machine.
Кроме того, контейнеры (в отличие от виртуальных машин) используют ядро совместно с хостом, поэтому эксплойт ядра, запущенный внутри контейнера, напрямую выполняется в ядре хоста. Например, эксплойт повышения привилегий в ядре (к примеру, Dirty COW), запущенный внутри хорошо изолированного контейнера, приведёт к root-доступу на хосте.
Правило 1
Не давайте доступ к сокету демона Docker
Cлужба (демон) Docker использует UNIX сокет /var/run/docker.sock для входящих соединений API. Владельцем данного ресурса должен быть пользователь root. И никак иначе. Изменение прав доступа к этому сокету по сути равносильно предоставлению root-доступа к хостовой системе.
Также не следует шарить сокет /var/run/docker.sock с контейнерами, там где без этого можно обойтись, поскольку в таком случае компрометация сервиса в контейнере приведет к полному контролю над хостовой системой. Если у вас есть контейнеры, которые используют примерно такую настройку:
-v /var/run/docker.sock://var/run/docker.sock
или для docker-compose:
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"
необходимо срочно это изменить.
И последнее — никогда, слышите, никогда не используйте Docker TCP сокет без абсолютной уверенности, что вам это нужно, особенно без использования дополнительных методов защиты (хотя бы авторизации). По умолчанию Docker TCP сокет открывает порт на внешнем интерфейсе 0.0.0.0:2375 (2376, в случае HTTPs) и позволяет полностью контролировать контейнеры, а вместе с ним потенциально и хостовую систему.
Правило 2
Настраивайте непривилегированного пользователя внутри контейнера
Настройка контейнера на использование непривилегированного пользователя — лучший способ избежать атаки на повышение привилегий. Это можно сделать различными способами:
1. Используя опцию «-u» команды «docker run»:
docker run -u 4000 alpine
2. Во время сборки образа:
FROM alpine
RUN groupadd -r myuser && useradd -r -g myuser myuser
<Здесь ещё можно выполнять команды от root-пользователя, например, ставить пакеты>
USER myuser
3. Включить поддержку «user namespace» (пользовательского окружения) в Docker daemon:
--userns-remap=default
Подробнее об этом в официальной документации.
В Kubernetes последнее настраивается в Security Context через опцию runAsNonRoot:
kind: ...
apiVersion: ...
metadata:
name: ...
spec:
...
containers:
- name: ...
image: ....
securityContext:
...
runAsNonRoot: true
...
Правило 3
Ограничивайте возможности контейнера
В Linux, начиная с ядра 2.2, появился способ контролировать возможности привилегированных процессов под названием Linux Kernel Capabilities (подробности по ссылке).
Docker по умолчанию использует предустановленный набор этих возможностей ядра. И позволяет менять этот набор при помощи команд:
--cap-drop — отключает поддержку возможности ядра
--cap-add — добавляет поддержку возможности ядра
Лучшая в плане безопасности настройка — это сначала отключение всех возможностей (--cap-drop all), а потом уже подключение только необходимых. Например так:
docker run --cap-drop all --cap-add CHOWN alpine
И самое важное (!): избегайте запуска контейнеров с флагом —privileged!!!
В Kubernetes ограничение Linux Kernel Capabilities настраивается в Security Context через опцию capabilities:
kind: ...
apiVersion: ...
metadata:
name: ...
spec:
...
containers:
- name: ...
image: ....
securityContext:
...
capabilities:
drop:
- all
add:
- CHOWN
...
Правило 4
Используйте флаг no-new-privileges
При запуске контейнера полезно использовать флаг --security-opt=no-new-privileges который предотвращает повышение привилегий внутри контейнера.
В Kubernetes ограничение Linux Kernel Capabilities настраивается в Security Context через опцию allowPrivilegeEscalation:
kind: ...
apiVersion: ...
metadata:
name: ...
spec:
...
containers:
- name: ...
image: ....
securityContext:
...
allowPrivilegeEscalation: false
...
Правило 5
Отключайте межконтейнерное взаимодействие
По умолчанию в Docker включено межконтейнерное взаимодействие, это означает, что все контейнеры могут взаимодействовать между собой (используя сеть docker0). Эта возможность может быть отключена путём запуска Docker сервиса с флагом —icc=false.
Правило 6
Используйте модули безопасности Linux (Linux Security Module – seccomp, AppArmor, SELinux)
По умолчанию Docker уже использует профили для модулей безопасности Linux. Поэтому никогда не отключайте профили безопасности! Максимум, что можно с ними делать – ужесточать правила.
Профиль по умолчанию для seccomp доступен здесь.
Docker также использует AppArmor для защиты, притом Docker Engine сам генерирует дефолтный профиль для AppArmor при запуске контейнера. Другими словами, вместо:
$ docker run --rm -it hello-world
запускается:
$ docker run --rm -it --security-opt apparmor=docker-default hello-world
Также в документации приведён пример профиля AppArmor для nginx, который вполне можно (нужно!) использовать:
#include <tunables/global>
profile docker-nginx flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
network inet tcp,
network inet udp,
network inet icmp,
deny network raw,
deny network packet,
file,
umount,
deny /bin/** wl,
deny /boot/** wl,
deny /dev/** wl,
deny /etc/** wl,
deny /home/** wl,
deny /lib/** wl,
deny /lib64/** wl,
deny /media/** wl,
deny /mnt/** wl,
deny /opt/** wl,
deny /proc/** wl,
deny /root/** wl,
deny /sbin/** wl,
deny /srv/** wl,
deny /tmp/** wl,
deny /sys/** wl,
deny /usr/** wl,
audit /** w,
/var/run/nginx.pid w,
/usr/sbin/nginx ix,
deny /bin/dash mrwklx,
deny /bin/sh mrwklx,
deny /usr/bin/top mrwklx,
capability chown,
capability dac_override,
capability setuid,
capability setgid,
capability net_bind_service,
deny @{PROC}/* w, # deny write for all files directly in /proc (not in a subdir)
# deny write to files not in /proc/<number>/** or /proc/sys/**
deny @{PROC}/{[^1-9],[^1-9][^0-9],[^1-9s][^0-9y][^0-9s],[^1-9][^0-9][^0-9][^0-9]*}/** w,
deny @{PROC}/sys/[^k]** w, # deny /proc/sys except /proc/sys/k* (effectively /proc/sys/kernel)
deny @{PROC}/sys/kernel/{?,??,[^s][^h][^m]**} w, # deny everything except shm* in /proc/sys/kernel/
deny @{PROC}/sysrq-trigger rwklx,
deny @{PROC}/mem rwklx,
deny @{PROC}/kmem rwklx,
deny @{PROC}/kcore rwklx,
deny mount,
deny /sys/[^f]*/** wklx,
deny /sys/f[^s]*/** wklx,
deny /sys/fs/[^c]*/** wklx,
deny /sys/fs/c[^g]*/** wklx,
deny /sys/fs/cg[^r]*/** wklx,
deny /sys/firmware/** rwklx,
deny /sys/kernel/security/** rwklx,
}
Правило 7
Ограничивайте ресурсы контейнеров
Это правило довольно простое: во имя предотвращения пожирания контейнерами всех ресурсов сервера во время очередной DoS/DDoS атаки мы можем настроить лимиты использования памяти для каждого контейнера в отдельности. Ограничивать можно: количество памяти, CPU, количество перезапусков контейнера.
Итак, пойдём по порядку.
Память
Опция -m или --memory
Максимальное количество памяти, которое может использовать контейнер. Минимальное значение – 4m (4 мегабайта).
Опция --memory-swap
Опция для настройки swap (файла подкачки). Настраивается хитро:
- Если --memory-swap > 0, тогда необходимо, чтобы и флаг –memory был установлен. В таком случае memory-swap показывает, сколько всего памяти доступно контейнеру вместе со swap.
- Проще на примере. Если --memory=«300m», а --memory-swap=«1g», то контейнер может использовать 300Мб памяти и 700Мб swap (1g — 300m).
- Если --memory-swap=0, настройка игнорируется.
- Если --memory-swap установлено в то же значение, что и --memory, то у контейнера не будет swap’а.
- Если значение --memory-swap не задано, а --memory задано, то количество swap будет равно удвоенному количеству заданной памяти. Например, если --memory=«300m», а --memory-swap не задан, то контейнер будет использовать 300Мб памяти и 600Мб swap.
- Если --memory-swap=-1, то контейнер будет использовать весь swap, который возможен на хостовой системе.
Хозяйке на заметку: утилита free, запущенная внутри контейнера, показывает не реальное значение доступного swap для контейнера, а количество swap хоста.
Опция --oom-kill-disable
Позволяет включать или выключать OOM (Out of memory) killer.
Внимание! Выключать OOM Killer можно только при заданной опции --memory, иначе может случиться так, что при out-of-memory внутри контейнера ядро начнёт убивать процессы хостовой системы.
Остальные опции настройки управления памятью, такие как --memory-swappiness, --memory-reservation и --kernel-memory больше служат для тюнинга производительности контейнера.
Процессор
Опция --cpus
Опция устанавливает, сколько доступных ресурсов процессора может использовать контейнер. Например, если у нас хост с двумя CPU и мы зададим --cpus=«1.5», то контейнеру гарантировано использование полутора процессора.
Опция --cpuset-cpus
Настраивает использование конкретных ядер или CPU. Значение может быть задано через дефис или через запятую. В первом случае будет указан диапазон разрешённых ядер, во втором — конкретные ядра.
Количество перезапусков контейнера
--restart=on-failure:<number_of_restarts>
Эта настройка устанавливает, сколько раз Docker попытается перезапустить контейнер в случае его неожиданного падения. Счётчик обнуляется, если состояние контейнера перешло в «запущен».
Рекомендуется ставить небольшое положительное число, например, 5, что позволит избежать бесконечных перезапусков не работающего сервиса.
Правило 8
Используйте read-only файловые системы и volume
Если контейнер не должен что-либо куда-либо писать, то нужно максимально, где это возможно, использовать read-only файловую систему. Это сильно усложнит жизнь потенциальному нарушителю.
Пример запуска контейнера с read-only файловой системой:
docker run --read-only alpine
Пример подключения volume в режиме read-only:
docker run -v volume-name:/path/in/container:ro alpine
Правило 9
Используйте инструменты анализа безопасности контейнеров
Необходимо применять инструменты для обнаружения контейнеров с уже известными уязвимостями. Их пока не сильно много, но они есть:
• Бесплатный:
• Коммерческие:
- Snyk (есть бесплатная версия);
- anchore (есть бесплатная версия);
- JFrog XRay;
- Qualys.
А для Kubernetes существуют инструменты для выявления ошибок конфигураций:
powerman
Да? Может подскажете заодно, как именно? Это настраивается только для тех контейнеров, которые без этого не могут выполнять свою работу — service discovery новых контейнеров, управление докером для запуска контейнеров, etc. Если такой функционал необходим — что надо изменить, чтобы всё продолжало работать, но более безопасным образом?
de1m
Никак это не меняется. В целом автор прав, но как и всегда есть исключения.
square
Вот тоже не люблю подобные утверждения без альтернативы, звучит как: — переходить дорогу на красный очень опасно, поэтому не советуем вам вообще её переходить, сидите дома.
Acribia Автор
Спасибо, немного изменили текст, чтобы не быть столь категоричными. Однако количество случаев, когда это действительно нужно значительно ниже, количество случаев, когда без этой настройки можно было бы и обойтись. На наш взгляд, гораздо лучше говорить людям, что так делать не стоит. Те, кто действительно понимают, что делают, проигнорируют такие советы, а те, кто не понимают, не наступят на грабли лишний раз.
powerman
Не знаю, я никогда не встречал инструкций так делать на докерхабе для образов, которым этот доступ не был нужен. А запускают обычно по этим инструкциям.