Безопасность сборки и CI/CD

Пайплайн CI/CD — это мост между исходником и продом, и потому одна из самых ценных целей для атаки.

Безопасность сборки — это защита самого процесса CI/CD: его секретов, прав, изоляции и целостности выходного артефакта, чтобы между «коммит прошёл ревью» и «релиз поехал в прод» никто не подменил код.

Парадокс CI/CD: пайплайн по определению имеет высокие привилегии — доступ к секретам, к реестрам, к продакшену. При этом он выполняет код, который может прийти из зависимостей, форков или скриптов. Если атакующий контролирует сборку, он может внедрить вредоносный код прямо в подписанный, «доверенный» артефакт — и обычное код-ревью этого не заметит.

Зачем это знать защитнику

Компрометация CI/CD — это атака на цепочку поставок изнутри. Злоумышленнику не нужно ломать прод напрямую: достаточно протащить изменение в сборку, и оно само разъедется по всем потребителям с легитимной подписью. Поэтому пайплайн защищают как продакшен-систему: минимум прав, изоляция, защита секретов и контроль того, что попадает в артефакт.

Изоляция сборки

Каждый запуск сборки должен идти в чистом, эфемерном окружении, которое уничтожается после завершения. Зачем: если сборки делят одну машину, вредоносный шаг в одном проекте может оставить «закладку» (модифицированный кеш, подменённый бинарь компилятора), которая повлияет на следующую сборку. Эфемерность разрывает эту связь — каждая сборка начинается с чистого листа.

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

Защита секретов в пайплайне

Секреты (токены реестра, ключи деплоя, пароли БД) — главная добыча. Базовые правила:

  • Никогда не хардкодьте секреты в YAML пайплайна или в репозитории. Используйте хранилище секретов CI или внешний vault, подставляя значения как переменные окружения на момент шага.
  • Маскируйте вывод. CI должен затирать значения секретов в логах, иначе echo $TOKEN или случайный дамп окружения утечёт в общедоступные логи сборки.
  • Минимальный охват. Секрет должен быть доступен только тому шагу/окружению, которому он реально нужен, и иметь как можно более узкие права (например, токен только на push в один конкретный репозиторий).
  • Короткоживущие учётки. Вместо вечного токена в настройках лучше получать временные креды через OIDC-федерацию: пайплайн предъявляет облаку свою подписанную личность и получает токен на минуты. Утекать там почти нечему.
# Иллюстрация: секрет берётся из хранилища, а не из репозитория
steps:
  - name: Deploy
    env:
      DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}   # из защищённого хранилища CI
    run: ./deploy.sh    # внутри НЕ печатать $DEPLOY_TOKEN в лог

Права раннеров: принцип наименьших привилегий

Раннер (агент, выполняющий сборку) часто имеет слишком широкие права «на всякий случай». Это опасно. Применяйте least privilege:

  • Токен пайплайна по умолчанию делайте read-only; повышайте права точечно для конкретной задачи (например, право на запись пакета — только в job публикации).
  • Не давайте раннерам для сборки прав на изменение настроек репозитория, защиту веток или управление другими пайплайнами.
  • Изолируйте сборки из форков и недоверенных PR: они не должны иметь доступа к боевым секретам. Иначе кто угодно открывает PR со скриптом, читающим ваши токены.

Защита артефакта от внедрения

Цель атакующего — чтобы вредоносный код оказался в финальном артефакте незаметно. Защита:

  • Пиннингуйте инструменты сборки. Сторонние шаги/плагины пайплайна фиксируйте по точному коммиту (хешу), а не по изменяемому тегу — иначе обновлённый «доверенный» шаг может оказаться скомпрометированным.
  • Двухфазная модель. Разделяйте шаги, которые исполняют недоверенный код (тесты, сборка из PR), и шаги, у которых есть секреты/право на релиз. Первые не должны иметь доступа ко вторым.
  • Фиксируйте provenance. Пусть сборка выпускает подписанное утверждение «этот артефакт собран из коммита C пайплайном P» (см. предыдущий урок). Тогда подмену артефакта на финальном шаге можно обнаружить проверкой.
  • Защищайте ветки. Релиз должен собираться только из защищённой ветки, в которую нельзя пушить напрямую — лишь через ревью. Это закрывает путь «протолкнуть код в сборку мимо ревью».

Как это работает под капотом

Уязвимость CI/CD возникает на стыке привилегий и недоверенного ввода. Пайплайн запускается с правами, достаточными для деплоя, но среди его шагов есть исполнение кода, который контролирует не только владелец репозитория: команды из package.json-скриптов зависимостей, содержимое тестов из присланного PR, сторонние actions/плагины. Если хоть один такой шаг видит боевые секреты или может писать в артефакт, граница доверия нарушена. Защита — это всегда разрыв этой связи: либо лишить недоверенный шаг привилегий (least privilege, изоляция форков), либо лишить привилегированный шаг недоверенного ввода (двухфазная модель, пиннинг шагов).

Как защититься

  • Запускайте сборки в чистом эфемерном окружении с ограниченной сетью.
  • Храните секреты в vault/хранилище CI, маскируйте их в логах, давайте узкий охват и предпочитайте короткоживущие OIDC-креды.
  • Раннеры — по принципу наименьших привилегий: read-only по умолчанию, изоляция форков и недоверенных PR от боевых секретов.
  • Защищайте артефакт: пиннинг сторонних шагов по хешу, двухфазная модель, provenance, защищённые ветки для релиза.

Юридическое напоминание: несанкционированное вмешательство в работу информационных систем и внедрение вредоносного кода наказуемы (ст. 272–274 УК РФ). Все техники здесь рассматриваются для защиты собственных пайплайнов; тестировать чужие CI/CD можно только с письменного разрешения владельца.

Итоги

  • CI/CD — привилегированный мост в прод, и компрометация сборки позволяет внедрить вредоносный код в подписанный артефакт мимо код-ревью.
  • Изоляция (эфемерные раннеры, ограниченная сеть) разрывает связь между сборками и мешает выгрузке секретов.
  • Секреты защищают хранилищем, маскированием, узким охватом и короткоживущими OIDC-кредами вместо вечных токенов.
  • Целостность артефакта обеспечивают least privilege раннеров, изоляция форков, пиннинг шагов по хешу, двухфазная модель и provenance.
Проверьте себя
1. Почему сборки из форков и недоверенных pull request нужно изолировать от боевых секретов?
AИначе сборка форка будет идти слишком медленно
BИначе любой человек, открыв PR с произвольным скриптом, сможет прочитать секреты пайплайна и токены
CПотому что форки нельзя пиннинговать по хешу
DПотому что секреты в форках всегда хранятся в открытом виде
2. Зачем фиксировать (пиннинговать) сторонние шаги пайплайна по точному коммиту-хешу, а не по тегу?
AЧтобы пайплайн запускался быстрее из кеша
BЧтобы обновлённый под тем же тегом (и потенциально скомпрометированный) шаг не попал в вашу сборку автоматически
CПотому что теги в CI запрещены политикой
DЧтобы уменьшить размер лог-файлов сборки