Привет, Хабр! На связи команда безопасности Платформы в лице её тимлида Букина Владимира. Основная задача нашей команды — защита CI/CD и, в частности, GitLab с K8s. Дальше я расскажу вам о том, как мы внедряли, поддерживаем и улучшаем наш плагин авторизации для Docker socket.
Так сложилось в нашей индустрии, что ИБ всегда догоняет технологии, которые внедрили в IT. При внедрении технологии всплывают всевозможные риски, о которых не успели подумать при разработке. Для мира ИБ Docker и K8s — ещё совсем свежие технологии. Исследований не так много. Всё ещё куча уязвимостей (в том числе и необнаруженных), и поэтому поработать с ними особенно интересно.
В статье хочу рассказать о том, как мы сделали наши CI/CD-процессы более безопасными: в частности, про shared Docker executor и использование Open Policy Agent (OPA). Поделюсь нашими правилами для ОРА-плагина, которые можно переиспользовать в любой компании для того, чтобы обезопасить ваши контейнеры.
Статья будет особенно полезна инженерам ИБ, DevOps-инженерам, архитекторам и СТО, но и разработчики найдут для себя что-то интересное, я уверен.
Погнали!
Shared executor в CI/CD: типы и риски
Давайте разберёмся, о чём, вообще, речь. Для выстраивания CI/CD зачастую используется GitLab. Мы не стали исключением и тоже пошли по этому пути. Для выстраивания CI используется .gitlab-ci.yaml, в котором вы описываете инструкции для сборки — джобы. Они в свою очередь исполняются на executor или, как их ещё называют, gitlab runners.
Есть разные типы executor:
Shell — все джобы запускаются в рамках ОС от имени одного пользователя;
Docker — каждая джоба запускается в контейнере на демоне Docker. Чтобы работать с Docker, в джобу пробрасывается Docker API socket;
Kubernetes — каждая джоба запускается в своём поде.
Например: чтобы среда сборки была предсказуемой и воспроизводимой, сборку нужно запускать в контейнерах или виртуальных машинах, которые пересоздаются после каждой сборки. В GitLab CI это может достигаться за счёт использования для сборок Docker executor'а. Docker executor запускает Docker-контейнер на GitLab Runner'е, внутри которого уже и исполняются shell-инструкции, описанные в GitLab CI.
Вот такие разделяемые executor в CI/CD приносят в наши процессы потенциальные угрозы для ИБ. Давайте обсудим, что может случиться. В GitLab, помимо обычного executor, есть разделяемый. Это executor, доступный любой команде, которая имеет доступ в CI/CD.
Используя его случайно или умышленно, одна команда может атаковать джобу другой команды. Или представим ситуацию, где взломали какого-то сотрудника команды. Это особо опасно в multitenancy-окружении, когда у нас в одном CI/CD могут «жить» несколько команд и между ними нет доверия.
А теперь посмотрим, в каких взаимоотношениях могу оказаться команды:
У одной команды есть доступ к коду другой.
Чем грозит: бывают случаи, когда код — это всё-таки ценность и его нельзя раскрывать никому, даже соседней команде.Одной команде доступны артефакты других команд и возможность их перезаписать.
Чем грозит: если в джобах создаются какие-либо артефакты, они пушатся в registry компании, и если одна команда может перезаписать артефакт другой команды, то на проде окажется не то, что ожидалось.У команды есть доступ к секретам, используемым в джобах других команд.
Чем грозит: в джобах бывают секреты, которые нужны, например, для пуша артефактов в registry. Если украсть секрет, то мы получим возможность перезаписи из пункта выше.У команды есть повышенные привилегии на executor.
Чем грозит: это позволяет добиться всего вышесказанного, то есть, если нам удалось получить повышенные привилегии, выйти из контейнера в Docker или Kubernetes и там получить root, например, в случае shell, то понятно, что у атакующего будет больше возможностей.
Далее рассмотрим эти угрозы по типам executor.
Shell
Чтение кода: возможно
Доступ к артефактам: возможен
Доступ к секретам: возможен
Повышение привилегий: в общем-то не требуется
Последствия
Когда джоба запускается в executor, в определённую папку монтируется исходный код и до того, как он удалился, можно успеть его прочитать.
Поскольку мы запускаемся в ОС от имени того же пользователя, у нас есть права на чтение. А если есть права и на запись, то мы можем перезаписать артефакты, которые собираются в этой папке.
Можем читать и секреты, которые могут быть доступны как в файле, так и в памяти запущенного процесса executorа, потому что раз процессы, запущенные от имени того же пользователя, что и наш, можем читать и анализировать их память, добывать оттуда секреты.
Docker
Чтение кода: возможно через Docker API socket
Доступ к артефактам: возможен через Docker API socket
Доступ к секретам: возможен через Docker API socket
Повышение привилегий: возможно через docker run -- privileged
Последствия
Основная проблема Docker заключается в том, что он пробрасывает свой API socket в джобу, если так происходит, то мы получаем цепочку, аналогичную первому пункту:
чтение кода через docker cp;
пуш артефактов через docker cp в другую сторону;
доступ к секретам через docker exec;
повышение привилегий через запуск привилегированного контейнера при использовании команды docker run — privileged. Процесс запускается на хостовой ОС, который ничем не ограничен. Это грозит нам несанкционированным чтением и изменением файловой системы.
Kubernetes (*)
(*) — в рамках статьи рассмотрим только использовании dind (Docker in Docker).
Если у нас используется dind, то мы имеем всё вышеперечисленное и даже больше, чем на Docker executor.
Последствия
По умолчанию можно повысить привилегии и, если это сделал злоумышленник, то происходит не только взлом хоста, но и взлом всего кластера. Атакующий выходит на ноду и повышает привилегии в кластере.
Я бы хотел отметить, что в CI/CD небезопасный нюанс использования Docker, поскольку он имеет, как правило, избыточный функционал, то есть это одновременно инструмент для сборки и runtime. А в CI/CD нужен только инструмент для сборки, runtime не нужен.
Есть другие инструменты: kaniko и buildah. Они позволяют собирать OCI-образы, но не позволяют их запускать, так как им не требуется демон Docker. Но если всё-таки душа просит именно Docker и executor в CI/CD, то тогда стоит обратить свой взгляд на плагин OPA.
Open Policy Agent (OPA)
Open Policy Agent (OPA) — единый набор инструментов и фреймворк для разработки политики в рамках встроенного облачного стека c открытым исходным кодом.
Один из инструментов — плагин, который позволяет создавать разрешающие и запрещающие правила на основании запроса, отправленного в Docker API socket. Когда я искал готовые решения для валидации API-запросов, то ничего подходящего не нашёл, и пришлось писать свои правила.
И вот, что у меня из этого получилось
Сначала поговорим о том, какие команды нам нужно запретить в демоне Docker, чтобы, с одной стороны, сделать всю эту конструкцию более безопасной, но при этом Docker API socket остался проброшенным в разделяемом Docker executor.
Для этого нам нужно запретить:
возможность влиять на сам демон Docker (запретить системные вызовы демона Docker);
Swarm;
возможность создания привилегированных (*) контейнеров и, соответственно, выхода из докера;
возможность монтирования файлов/директорий с хостовой ОС;
возможность читать данные в другом контейнере;
возможность внедряться в чужой контейнер.
С чем работает OPA-плагин?
Плагин получает разобранный HTTP-запрос:
Method
Path
Query
Headers
Body
Поэтому:
1. OPA — читаем документацию Docker
Чтобы понять, как написать Rego-правила, которые будут запрещать всё то, что нам не нужно, обратимся к официальной документации Docker API. Она гласит, что нельзя упустить ни одного API, иначе можно будет как-то пролезть через них.
2. OPA — влияние на демон
Далее будут приведены команды Docker, которые вы, наверное, видели. И первое, что мы запрещаем, — это docker plugin, потому что с помощью этой команды можно просто отключить OPA.
Подсказка! Выполняется это следующим образом:
получаем список используемых плагинов docker plugin ls --filter enabled=true;
выключаем OPA или любой другой docker plugin disable --force <plugin_name>;
запрещаем docker swarm и на всякий случай docker volumes;
с точки зрения запуска контейнеров мы должны запретить:
--privileged (привилегированный запуск);
--cap-add (запуск с капабилити);
--ipc (IPC namespace);
--pid (PID namespace);
--network (Network namespace);
-v (монтирование volume);
--cgroup-parent;
--device (подключение device);
--security-opt apparmor/seccomp (отключение apparmor/seccomp).
3. OPA — ограничения на работу с другими контейнерами
Что касается работы с другими контейнерами, можем запретить:
docker exec;
docker cp;
docker stop/kill/pause/restart;
docker update;
docker attach/logs;
docker commit/checkpoint.
Составляем Rego-правило
Это правило, которое запрещает при создании контейнера прокидывать в него девайсы. Оно пропускает запрос, только если в body есть хост-конфиг с параметром not devices или же null.
OPA — исключения
-p — порты на хосте;
docker stop (с точки зрения модели угроз влияет лишь на доступность раннера, код не утечёт, но CI/СD может встать);
docker attach/logs (чтение логов разрешили, но это допустимо при наличии дополнительной проверки на отсутствие конфиденциальной информации в логах).
OPA — админский пароль для байпаса
Если вдруг админу нужно что-то сделать с Docker, то можно добавить определённый заголовок, который будет проверяться Rego-правилом. При использовании специального админского заголовка не будут применяться никакие другие проверки.
А теперь проиллюстрируем вышесказанное примерами:
Ограничение pid ns
package docker.authz
allow {
not pid
}
pid { #only string values "" are allowed
not is_string(input.Body.HostConfig.PidMode)
} {
input.Body.HostConfig.PidMode != ""
}
Ограничение seccomp_apparmor
package docker.authz
allow {
not seccomp_apparmor_unconfined
}
seccomp_apparmor_unconfined {
contains(input.Body.HostConfig.SecurityOpt[_], "unconfined")
}
Ограничение exec
package docker.authz
allow {
not exec
}
exec {
val := input.PathArr[_]
val == "exec"
}
Выводы
Использование Docker executor в CI/CD-процессах несет значительные риски, особенно в многопользовательских и многоуровневых окружениях, где важна изоляция между командами.
Высокий уровень привилегий, предоставляемый Docker, создаёт множество точек уязвимости. Хотя существуют альтернативы, такие как Kaniko и Buildah, которые убирают излишний функционал Docker, его востребованность остается высокой.
Поэтому, если выбор в пользу Docker executor неизбежен, крайне важно его обезопасить. Использование Open Policy Agent (OPA) с продуманными Rego-правилами позволяет создать необходимый уровень защиты, ограничивая доступ к критичным функциям и предотвращая нежелательные действия.
Внедрив OPA, можно избежать множества потенциальных угроз, сохраняя при этом гибкость Docker в CI/CD.
А составленные нами правила для ОРА-плагина посодействуют на пути внедрения.
THE END
P.S. Для нашей команды инициатором обратиться к OPA стал Паша Сорокин. Ему за это отдельное спасибо!
Комментарии (8)
slonopotamus
31.10.2024 14:34Основная проблема Docker заключается в том, что он пробрасывает свой API socket в джобу
Эээ... Но ведь нет. И ложность этого утверждения умножает на ноль смысл всего последующего текста.
Но если всё-таки душа просит именно Docker...
... то может быть она это как-то аргументирует или пойдёт нафиг со своими запросами?
valery1707
31.10.2024 14:34Но если всё-таки душа просит именно Docker...
... то может быть она это как-то аргументирует или пойдёт нафиг со своими запросами?Мы планируем при сборке гонять тесты требующие подъёма зависимостей - для этого нужно чтобы каждый сборщик мог запустить свой набор контейнеров.
ugenk
31.10.2024 14:34А чем dind не подошел?
slonopotamus
31.10.2024 14:34dind - это обман. Контейнеры не могут быть вложенными из-за ограничений на уровне ядра.
Negativelink
31.10.2024 14:34Когда-то давно в моей практике был случай, когда небезопасный докер-контейнер в инфраструктуре привел к крупному взлому. Собственно, я занимался уже разбором последствий. Неправильно приготовленные контейнеры это опасность.
Хорошая статья.
ml_or_lm
31.10.2024 14:34Помню как года 3-4 назад для большинства это был темный лес с точки зрения безопасности.
Mexico1821
Знаком с Пашей! Классно, что вклад в работу настолько заметен.
Спасибо Владимиру за приведенные примеры!