Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
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 deamon:
--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
Используйте инструменты анализа безопасности контейнеров.
Необходимо применять инструменты для обнаружения контейнеров с уже известными уязвимостями. Их пока не сильно много, но они есть:
• Бесплатный:
- Clair.
• Коммерческие:
- Snyk (есть бесплатная версия);
- anchore (есть бесплатная версия);
- JFrog XRay;
- Qualys.
А для Kubernetes существуют инструменты для выявления ошибок конфигураций:
- kubeaudit;
- kubesec.io;
- kube-bench.