Привет, Хабр! На связи команда безопасности Платформы в лице её тимлида Букина Владимира. Основная задача нашей команды — защита 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 могут «жить» несколько команд и между ними нет доверия.

А теперь посмотрим, в каких взаимоотношениях могу оказаться команды:

  1. У одной команды есть доступ к коду другой.
    Чем грозит: бывают случаи, когда код — это всё-таки ценность и его нельзя раскрывать никому, даже соседней команде.

  2. Одной команде доступны артефакты других команд и возможность их перезаписать.
    Чем грозит: если в джобах создаются какие-либо артефакты, они пушатся в registry компании, и если одна команда может перезаписать артефакт другой команды, то на проде окажется не то, что ожидалось.

  3. У команды есть доступ к секретам, используемым в джобах других команд.
    Чем грозит: в джобах бывают секреты, которые нужны, например, для пуша артефактов в registry. Если украсть секрет, то мы получим возможность перезаписи из пункта выше.

  4. У команды есть повышенные привилегии на 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.

Подсказка! Выполняется это следующим образом:

  1. получаем список используемых плагинов docker plugin ls --filter enabled=true;

  2. выключаем OPA или любой другой docker plugin disable --force <plugin_name>;

  3. запрещаем docker swarm и на всякий случай docker volumes;

  4. с точки зрения запуска контейнеров мы должны запретить:

  • --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)


  1. Mexico1821
    31.10.2024 14:34

    Знаком с Пашей! Классно, что вклад в работу настолько заметен.
    Спасибо Владимиру за приведенные примеры!


  1. slonopotamus
    31.10.2024 14:34

    Основная проблема Docker заключается в том, что он пробрасывает свой API socket в джобу

    Эээ... Но ведь нет. И ложность этого утверждения умножает на ноль смысл всего последующего текста.

    Но если всё-таки душа просит именно Docker...

    ... то может быть она это как-то аргументирует или пойдёт нафиг со своими запросами?


    1. valery1707
      31.10.2024 14:34

      Но если всё-таки душа просит именно Docker...
      ... то может быть она это как-то аргументирует или пойдёт нафиг со своими запросами?

      Мы планируем при сборке гонять тесты требующие подъёма зависимостей - для этого нужно чтобы каждый сборщик мог запустить свой набор контейнеров.


      1. ugenk
        31.10.2024 14:34

        А чем dind не подошел?


        1. slonopotamus
          31.10.2024 14:34

          dind - это обман. Контейнеры не могут быть вложенными из-за ограничений на уровне ядра.


      1. slonopotamus
        31.10.2024 14:34

        services?


  1. Negativelink
    31.10.2024 14:34

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

    Хорошая статья.


    1. ml_or_lm
      31.10.2024 14:34

      Помню как года 3-4 назад для большинства это был темный лес с точки зрения безопасности.