Безопасность в Docker: от правильной настройки хоста до демона

Безопасность в Docker: от правильной настройки хоста до демона

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

Изображение записи

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

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

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

Дисклеймер: каждая рекомендация должна рассматриваться индивидуально, в зависимости от вашего кейса. Необязательно следовать советам «в полном объеме». Но скажу так: иногда лучше учесть больше сценариев и перестраховаться, чем закрыть глаза на, казалось бы, мелочи и потерпеть фиаско.

Ложное чувство безопасности

По сути, контейнеры — это процессы Linux с изоляцией и ограничением ресурсов, работающие на общем ядре операционной системы, то есть «контейнеризованные процессы». Можно сказать, что контейнер просто использует механизмы операционной системы, которая, в свою очередь, по умолчанию не подразумевает защищенности в привычном понимании.

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

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

- Docker-хост,
- Docker Daemon,
- Docker-образ, 
- Runtime контейнеров.
Источник.

Безопасность Docker-хоста

Хостовая система — это машина или операционная система, на которой работает Docker Daemon (в случае локального запуска), хранятся локальные копии загруженных образов и запускаются контейнеры.

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

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

Общие рекомендации по безопасности ОС

Есть общепризнанные практики для запуска контейнеров на Linux. Но важно отметить, что они применимы и для любой другой ОС.

Запускайте контейнеры на выделенном хосте. Не смешивайте контейнеризированные приложения с обычными. Они имеют совершенно разные архитектуры и циклы обновления. Использование обоих типов приложений на одной машине увеличит риски с точки зрения безопасности. 

Используйте Thin OS, минимальный дистрибутив хостовой ОС. Рекомендуется включать только необходимые для работы контейнеров компоненты, чтобы сократить поверхность атаки на хост. Простое правило: чем меньше компонентов установлено, тем меньше уязвимостей. 

Существует несколько «тонких» дистрибутивов операционных систем, специально предназначенных для запуска контейнеров. В их числе — RancherOS, Fedora CoreOS от Red Hat и Photon OS от VMware.

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

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

Используйте минимальный набор учетных записей. Это упрощает администрирование и делает более заметными попытки нелегитимного входа. Более того, при автоматизации развертывания нам не нужен дополнительный доступ на саму машину. 

Логиройте попытки входа в систему на уровне хоста. К этим данным можно обратиться при анализе атак.

Для более подробного изучения рекомендую послушать доклад о безопасности ядра Linux и изучить текст с 20 советами по тому, как надежно защитить ОС.

Аудит системы

Стоить уделить особое внимание аудиту хост-системы с помощью, например, инструмента Lynis и Docker Bench for Security — утилиты для автотестов Docker-систем на CIS Docker Benchmark. Эти инструменты позволят найти слабые места в конфигурации и получить рекомендации по их исправлению.

Безопасная конфигурация Docker Daemon

Итак, мы установили Docker на нашей хост-машине, запустили контейнеры и постарались соблюсти базовые рекомендации. Что дальше?

Контролируйте доступ пользователей к Docker

Вероятно, вы уже заметили, что не каждый пользователь системы может запустить контейнер и даже выполнить команду docker ps. Скорее всего, гайд, на который вы ориентировались при установке Docker, предлагал вам создать отдельного sudo-пользователя и добавить его в специальную группу docker.

dockerenjoyer@ubuntu$ docker ps
permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.24/containers/json": dial unix /var/run/docker.sock: connect: permission denied

Это связано с тем, что Docker-клиент не имеет доступа к Docker Daemon. Права на сокет /var/run/docker.sock — о нем поговорим подробнее ниже — имеют только пользователи с допуском администратора (root) и те, кто входит в группу docker.

dockerenjoyer@ubuntu$ ls -la /var/run/docker.sock
srw-rw---- 1 root docker 0 Feb 12 22:10 /var/run/docker.sock

Ограничение числа пользователей, имеющих доступ к Docker Daemon, — важная часть процесса повышения безопасности. Удалите лишних пользователей из группы docker и следите за тем, чтобы никто «просто так» в ней не появлялся.

Не предоставляйте доступ к сокету демона Docker

Как уже было сказано выше, каждый Docker-клиент, в том числе docker cli, обращается к Docker Daemon — для этого используется сокет var/run/docker.sock. При вызове команд docker ps, docker build и прочих клиент отправляет HTTP-запрос демону Docker, который, по сути, выполняет всю работу.

Самое главное: любой, у кого есть доступ к сокету, может отправлять инструкции Docker Daemon и имеет полный контроль над ним, контейнерами и другими объектами. Демон выполняется от имени суперпользователя и легко может собрать или запустить любое приложение. Следовательно, доступ к сокету Docker по своей сущности эквивалентен доступу с полномочиями sudo-пользователя на хосте.

Попробуем получить список всех контейнеров на хосте напрямую через сокет:

# curl -s --unix-socket /var/run/docker.sock http://localhost/containers/json | jq .
[
  {
	"Id": "bdeee2239e44b563939d7122ee3f73c0b27923de53bb212076ad62471b3b2098",
	"Names": [
  	"/thirsty_carson"
	],
	"Image": "wordpress",
	"ImageID": "sha256:7d59b122c499df4a2e6e428430035c84b95f16e5a5d3732be59676c494512b48",
	"Command": "docker-entrypoint.sh apache2-foreground",
	"Created": 1707901415,
	"Ports": [
  	{
    	"PrivatePort": 80,
    	"Type": "tcp"
  	}
	],
	"Labels": {},
	"State": "running",
	"Status": "Up 2 weeks",
	"HostConfig": {
  	"NetworkMode": "default"
	},
	"NetworkSettings": {
  	"Networks": {
    	"bridge": {
      	"IPAMConfig": null,
      	"Links": null,
      	"Aliases": null,
      	"MacAddress": "02:42:ac:11:00:02",
      	"NetworkID": "6d42ab2ce634d124f93a1f6619e43344c3dfd71a854e4a6217e2475ad7792e8c",
      	"EndpointID": "30fa0322f2d44654653267f5debfbd812a4a377a9b6a267bb337cfcb1857f703",
      	"Gateway": "172.17.0.1",
      	"IPAddress": "172.17.0.2",
      	"IPPrefixLen": 16,
      	"IPv6Gateway": "",
      	"GlobalIPv6Address": "",
      	"GlobalIPv6PrefixLen": 0,
      	"DriverOpts": null,
      	"DNSNames": null
    	}
  	}
	},
	"Mounts": [
  	{
    	"Type": "volume",
    	"Name": "0531a7aa47561b8ab5f123d8e98af801d8d90f42f6896cb208ae6319c1ca4c8a",
    	"Source": "",
    	"Destination": "/var/www/html",
    	"Driver": "local",
    	"Mode": "",
    	"RW": true,
    	"Propagation": ""
  	}
	]
  }
]

Важно отметить, что так мы можем не только читать, но изменять существующие контейнеры, запускать новые и даже влиять на их состояние. 

Попробуем остановить этот запущенный контейнер:

# docker ps
CONTAINER ID   IMAGE   	COMMAND              	CREATED   	STATUS   	PORTS 	NAMES
bdeee2239e44   wordpress   "docker-entrypoint.s…"   2 weeks ago   Up 2 weeks   80/tcp	thirsty_carson
# curl --unix-socket /var/run/docker.sock -XPOST http://localhost/containers/bdeee2239e44b563939d7122ee3f73c0b27923de53bb212076ad62471b3b2098/stop
# docker ps
CONTAINER ID   IMAGE 	COMMAND   CREATED   STATUS	PORTS 	NAMES

Возможно, здесь у вас возникнет вопрос: если у нас уже есть доступ к хосту, то в чем опасность доступа к сокету? Постараюсь показать на примере.

Возможна ситуация, когда внутри контейнера нужны права для работы с сокетом. И тогда делают то, что категорически делать нельзя: запускают новый контейнер и монтируют в него сокет Docker с хоста. Таким образом, позволяют через контейнер управлять самим демоном.

#Так делать нельзя!
docker run -it -v /var/run/docker.sock:/var/run/docker.sock myapp

Никогда не пробрасывайте сокет внутрь контейнера. Иначе у него появится возможность выполнять команды Docker и, как следствие, контролировать хост. Такая дыра в системе безопасности — просто праздник для злоумышленника. Ведь он может этим воспользоваться и получить удаленный доступ к shell контейнера.

# Запустили основной контейнер и установили Docker внутри
#docker run -it -v /var/run/docker.sock:/var/run/docker.sock --rm wordpress bash
root@e0d602c19573:/var/www/html# apt-get update > /dev/null
root@e0d602c19573:/var/www/html# apt-get install -y curl > /dev/null
root@e0d602c19573:/var/www/html# curl -fsSL https://get.docker.com -o install-docker.sh
root@e0d602c19573:/var/www/html# sh install-docker.sh > /dev/null 2>&1
root@e0d602c19573:/var/www/html# docker -v
Docker version 25.0.3, build 4debf41
root@e0d602c19573:/var/www/html# docker ps
CONTAINER ID   IMAGE   	COMMAND              	CREATED     	STATUS     	PORTS 	NAMES
e0d602c19573   wordpress   "docker-entrypoint.s…"   2 minutes ago   Up 2 minutes   80/tcp	condescending_germain
# Запустили новый контейнер внутри основного, открыли оболочку bash и смонтировали всю файловую систему хоста в /mnt
root@e0d602c19573:/var/www/html# docker run -it -v /:/mnt ubuntu:22.04 bash
Unable to find image 'ubuntu:22.04' locally
22.04: Pulling from library/ubuntu
01007420e9b0: Pull complete
Digest: sha256:f9d633ff6640178c2d0525017174a688e2c1aef28f0a0130b26bd5554491f0da
Status: Downloaded newer image for ubuntu:22.04
root@90d8958c729d:/# ls /mnt
bdist.linux-x86_64  boot  etc   lib	lib64   lost+found  mnt  proc  run   snap  sys  usr
bin             	dev   home  lib32  libx32  media   	opt  root  sbin  srv   tmp  var
# Меняем основной каталог контейнера на /mnt
root@90d8958c729d:/# chroot /mnt
# Находим расшифрованный пароль пользователя
# grep dockerenjoyer /etc/shadow
dockerenjoyer:9cQoPO5M2VNT:19785:0:99999:7:::

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

Еще раз: никогда не пробрасывайте сокет в контейнеры! Обязательно найдется человек, который воспользуется этим и получит доступ ко всей системе.

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

Риски CI/CD и проблема демона Docker

Далеко не уходя от темы сокетов, отмечу, что сокет Docker очень часто монтируют в инструментах CI/CD — например, Jenkins и Gitlab-CI — для отправки инструкций по сборке образов как части пайплайна. Здесь также есть риски.

Например, разработчикам необходимо использовать Docker executor для отправки команд по сборке. Но тогда в контейнер, внутри которого крутится джоба, монтируется Docker-сокет. Это позволит злоумышленникам делать docker exec или docker cp, чтобы воровать секреты и подменять артефакты, а также запускать привилегированные контейнеры и «выбираться» на хост.

Хорошая практика в CI/CD, особенно в enterprise, — не использовать Docker. Одна из важных его проблем в безопасности — это объединение в себе двух абсолютно разных функционалов: сборки образов и управления рантаймом контейнеров. 

Чтобы избежать рисков и дыр в безопасности, лучше воспользоваться одной из альтернативных утилит для сборки образов контейнеров, не полагающихся на Docker Daemon. Например, инструментами, которые предназначены только для сборки. Среди них — BuildKit, kaniko и buildah и другие решения, которые работают без использования полномочий root-пользователя Именно такой подход желательно использовать в CI/CD вместо Docker.

Добавлю, что уже с 23.0 версии Docker BuildKit был встроен в билдер вместо устаревшего.

Ограничивайте риск эскалации привилегий

Docker Daemon может по умолчанию запускать контейнеры от имени суперпользователя, так как только у него есть достаточно привилегий для создания пространства имен. Но стартовать контейнер могут и пользователи из группы docker, у которых есть права на отправку команд через сокеты демону. 

Любой член данной группы может запускать контейнеры. А если смонтировать корневой каталог хоста с помощью команды docker run -v /:/host <образ>, то получим полный доступ к корневой файловой системе хоста.

Более того, Docker запускает контейнеры от root-пользователя, даже если не указать этого явно. Сочетание этих факторов предоставляет нам практически неограниченный доступ на хосте.

Лучший способ предотвратить атаки с эскалацией привилегий из контейнера — настроить запуск контейнера от имени непривилегированных пользователей. Как это сделать — рассмотрим в следующем разделе. А сейчас поговорим подробнее про ограничение «видимых» контейнеру ресурсов — пространств имен пользователей в Docker.

Напомню, что основой контейнеризации являются Linux namespace, которые позволяют изолировать и разделять системные ресурсы для процессов, тем самым — эффективно защитить хост от потенциально вредного влияния приложений, запущенных в контейнерах. Каждое пространство имен функционирует как независимый слой, ограничивая видимость и доступ к ресурсам системы для процессов.

Иногда все же могут быть причины, когда нужно «выполнять» контейнеры от root. Но это не значит, что нужно забывать о рисках под предлогом «исключения из правил». Мы можем ограничить неймспейс с помощью переназначения (re-map) root на менее привилегированного пользователя на хосте. 

Mapped пользователю присваивается ряд UID, которые функционируют в пространстве имен как обычные UID от 0 до 65536, но не имеют привилегий на самом хосте. Для этого у Docker есть параметр userns-remap, который по умолчанию отключен. Для большего понимания покажу на примере.

1. Запустим контейнер и выполним команду, чтобы посмотреть список запущенных процессов:

dockerenjoyer@ubuntu:~$  docker run -itd --name ubuntu1 ubuntu:22.04
dockerenjoyer@ubuntu:~$ docker exec -it ubuntu1 bash
root@93eb3b2d27d8:/# ps -u
USER     	PID %CPU %MEM	VSZ   RSS TTY  	STAT START   TIME COMMAND
root       	1  0.0  0.1   4628  3804 pts/0	Ss+  10:36   0:00 /bin/bash
root      	16  0.0  0.1   4628  3840 pts/1	Ss   10:38   0:00 bash
root      	23  0.0  0.0   7064  1560 pts/1	R+   10:38   0:00 ps -u

Как можно увидеть, процессы, запущенные в контейнере Docker, работают в контексте пользователя root.

2. Теперь проверим, как процессы в контейнере сопоставляются с процессами на хосте:

 dockerenjoyer@ubuntu:~$ docker container top ubuntu1
UID             	PID             	PPID            	C               	STIME           	TTY             	TIME            	CMD
root            	389168          	389145          	0               	10:36           	pts/0           	00:00:00        	/bin/bash

Процессы, запущенные в контейнере на хосте также работают в контексте пользователя root. Это позволяет злоумышленнику, «сбежавшему» из контейнера, получить root-доступ на хосте. Минимизировать этот риск можно с remapping. 

3. В файле //6f3bf64a-14d1-4b68-9202-2a000ca072b9.selcdn.net/etc/docker/daemon.json (если его нет, то создайте) укажем параметр userns-remap:

{
  "userns-remap": "default"
}

После установки userns-remap в значение default и перезапуска Docker система автоматически создаст пользователя с именем dockremap. Контейнеры будут запускаться в его контексте, а не от имени пользователя root.

4. Убедимся, что пользователь действительно был создан:

dockerenjoyer@ubuntu:~$ id dockremap
uid=111(dockremap) gid=119(dockremap) groups=119(dockremap)

dockerenjoyer@ubuntu:~$ cat /etc/subuid
dockerenjoyer:100000:65536
dockremap:165536:65536

Файл /etc/subuid говорит нам, какой подчиненный UID будет назначен в пространстве имен, где уникальное значение 165536 будет соответствовать UID 0 (root) в контейнере, 165537 — UID 1, 165538 — UID 2 и так далее.

5. Теперь повторим запуск контейнера:

dockerenjoyer@ubuntu:~$docker run -itd --name ubuntu1 ubuntu:22.04
dockerenjoyer@ubuntu:~$docker exec -it ubuntu1 bash
root@98cdca1cd725:/# ps -u
USER     	PID %CPU %MEM	VSZ   RSS TTY  	STAT START   TIME COMMAND
root       	1  0.0  0.1   4628  3700 pts/0	Ss+  10:53   0:00 /bin/bash
root       	8  0.0  0.1   4628  3768 pts/1	Ss   10:54   0:00 bash
root      	16  0.0  0.0   7064  1608 pts/1	R+   10:54   0:00 ps -u
root@98cdca1cd725:/# exit
exit
dockerenjoyer@ubuntu:~$ docker container top ubuntu1
UID             	PID             	PPID            	C               	STIME           	TTY             	TIME            	CMD
165536          	389598          	389575          	0               	10:53           	pts/0           	00:00:00        	/bin/bash

Может показаться, что ничего не изменилось, однако значительные изменения произошли после выполнения команды docker container top ubuntu1. Мы видим, что теперь, после внесенных изменений, процесс контейнера запущен на хосте в контексте недавно созданного непривилегированного пользователя dockeremap. Такая конфигурация значительно ограничивает возможность повышения привилегий в системе хоста.

Заключение

В рамках этой статьи мы обсудили не все аспекты. На очереди — безопасная сборка Docker-образов. Тема объемная, поэтому подробности обсудим в следующем материале.