Выстраиваем надежную защиту вокруг сборочных систем. Можно тщательно проверить каждую строку исходного кода, но при этом выпустить зараженную программу. Можно изолировать сервер от интернета, но оставить системе сборки возможность обновлять приложение на сервере. Можно скрыть пароль в защищенных настройках, а затем передать его процессу, который отправит пароль постороннему. В сфере разработки такие противоречия сходятся в одном месте — в конвейере CI/CD. CI (непрерывная интеграция) подразумевает регулярное сборку и проверку изменений с помощью автоматизированных тестов. CD (непрерывная доставка или развертывание) означает, что проверенный выпуск готов к развертыванию или автоматически попадает в рабочую среду. Конвейер объединяет исходный код, внешние компоненты, сборочные машины и инфраструктуру, где работает продукт. Для разработчика конвейер — это способ быстрее выпускать изменения. Для злоумышленника — это набор полномочий. Кто может запустить здесь свой код? Какие учетные данные получит процесс? Что разрешено менять с такими правами? Разберем путь от внешнего вклада до готового обновления и составим карту угроз, по которой можно проверять собственную инфраструктуру. ## Карта угроз начинается с границ доверия Схема «код, сборка, тесты, публикация» показывает порядок работы, но почти ничего не говорит о безопасности. Гораздо полезнее отметить переходы, на которых данные или команды получают новые права. Например, внешний участник предлагает изменение без доступа к инфраструктуре, а автоматическая проверка запускает предложенный код на машине с доступом к внутренним сервисам. Граница доверия проходит между участниками и средами с разными полномочиями. Публичный пакет, внутренняя библиотека, тестовый раннер и система публикации не должны автоматически доверять друг другу только потому, что участвуют в одном выпуске. Раннером называют программу, которая исполняет задания конвейера, а в разговорной практике также машину или среду, где задания работают. | Участок | Что контролирует атакующий | Что оказывается под угрозой | Где разорвать цепочку | |———|—————————|——————————|————————| | Репозиторий и запросы на изменение | Код, тесты, сборочные файлы или входные данные автоматизации | Выполнение команд с правами задания | Проверять внешний код в отдельной среде без полномочий выпуска | | Зависимости и вспомогательные инструменты | Версия пакета, установочный сценарий, компонент автоматизации | Файлы сборки и доступные процессу секреты | Контролировать обновления, источники и запускаемые сценарии | | Раннер и его окружение | Процесс, файловая система или образ сборочной машины | Другие задания, внутренняя сеть, результат сборки | Разделять среды и пересоздавать окружение после задания | | Секреты и служебные учетные записи | Код, которому выдали пароль, ключ или токен | Репозитории, облачные ресурсы, публикация пакетов | Выдавать минимальные права на короткое время | | Кеши и промежуточные результаты | Файлы, которые позже использует доверенное задание | Сборка или публикация подменённого содержимого | Разделять хранилища по уровню доверия и проверять происхождение файлов | | Подпись, публикация и развертывание | Публикуемый файл, указатель версии или право выпуска | Программа у клиентов и рабочие системы | Проверять конкретный артефакт и разрешённый путь его выпуска | Артефактом называют результат работы конвейера, например установочный пакет, исполняемый файл или контейнерный образ. Атака на цепочку поставок возникает, когда злоумышленник использует доверенный компонент или этап выпуска, чтобы добраться до следующего участника. Взлом сборочной машины может начаться как внутренний инцидент, а после публикации зараженного обновления превратиться в проблему клиентов. ## Первый вход. Чужой код получает права автоматизации Конвейер предназначен для запуска кода, поэтому атакующему необязательно искать уязвимость в самом сервере CI. Иногда достаточно воспользоваться штатной возможностью предложить изменение. Тест, сборочный сценарий или команда установки зависимости выполняются ещё до того, как разработчик разрешит включить изменения в основную ветку. Отсюда первая опасная ошибка. Команда считает внешний вклад недоверенным, но проверяет вклад в доверенном окружении. Отсутствие пароля рабочей базы в настройках задания ещё не означает изоляцию. У машины могут остаться служебные ключи, доступ к внутренней сети, общие каталоги или облачная роль, позволяющая получать временные учетные данные. В GitHub Actions отдельного внимания требуют события `pull_request_target` и `workflow_run`. При определённых настройках запущенные через них процессы имеют повышенные права. Если такой процесс загружает и исполняет код из недоверенного запроса, привилегии оказываются рядом с чужими командами. Название события само по себе не делает конфигурацию уязвимой, решает сочетание источника кода, доступных прав и выполняемых действий. Бывает и менее очевидный вход. Конвейер вставляет название запроса или другой пользовательский текст прямо в сценарий командной оболочки. При небезопасной подстановке текст превращается в команду. Практическое правило состоит из двух частей. Внешние значения нужно обрабатывать как данные, а проверку внешнего кода проводить без прав публикации. Подобные ошибки и способы защиты разобраны в документации GitHub. ## Зависимость может атаковать раньше, чем запустится приложение Внешняя библиотека попадает в проект не только по прямому выбору разработчика. Один пакет зависит от другого, второй подтягивает третий. Помимо библиотек самого приложения конвейер использует компиляторы, генераторы, тестовые инструменты и расширения автоматизации. Компонент может никогда не попасть к пользователю, но получить доступ к машине, которая выпускает продукт. Особенно опасны сценарии, которые менеджер пакетов запускает при установке. В npm к таким этапам относятся, в частности, `preinstall` и `postinstall`. Если запуск разрешён, вредоносная команда может выполниться до первого импорта библиотеки приложением. Поэтому аргумент «пакет нужен только для тестов» не снимает риск для сборочного окружения. В октябре 2021 года злоумышленники опубликовали заражённые версии ua-parser-js, библиотеки для разбора сведений о браузере и устройстве. Под удар попали версии 0.7.29, 0.8.0 и 1.0.0. Установочный сценарий запускал загрузку вредоносных компонентов, включая майнер, а в Windows также похититель паролей. Пользователь устанавливал настоящий пакет с привычным названием, поэтому внимательная проверка написания имени здесь не помогала. В сентябре 2025 года кампания Shai-Hulud показала следующий шаг. Червь распространялся через заражённые npm-пакеты, собирал секреты и использовал доступные полномочия для дальнейшего распространения. Возникала петля. Установка зависимости раскрывала учётные данные, а украденные права позволяли заражать другие пакеты. Защиту стоит строить вокруг точного состава сборки. Файл фиксации зависимостей, например `package-lock.json`, сохраняет выбранные версии, а `npm ci` устанавливает зависимости по нему и отказывается продолжать при несовместимости с манифестом проекта. Но зафиксированная вредоносная версия останется вредоносной. Контрольная сумма подтверждает соответствие ожидаемому содержимому, а не безопасность содержимого. Где проект позволяет, установочные сценарии можно отключать через `—ignore-scripts`. Ограничение нужно проверять на конкретной сборке, поскольку часть пакетов использует сценарии для законной подготовки компонентов. Запрет также не мешает вредоносному коду сработать позже, когда пакет явно запустят. Обновления зависимостей следует проверять, а установку выполнять в среде без секретов выпуска. Полный отказ от обновлений лишь закрепит старые уязвимости. ## Раннер превращается в плацдарм между заданиями После выполнения первой команды атакующего интересует окружение. Долгоживущая сборочная машина может хранить рабочие каталоги нескольких проектов, настройки доступа к реестрам пакетов и инструменты администрирования. Если недоверенное задание способно менять общую среду, последствия не обязательно закончатся вместе с заданием. Представим конфигурацию, в которой тестирование внешних изменений и выпуск продукта по очереди работают на одной машине под одной учётной записью. Первое задание меняет доступный ему вспомогательный файл, второе позже запускает файл уже с секретом публикации. На диаграмме задания разделены, но границы безопасности между заданиями нет. Контейнер помогает ограничить окружение, однако результат зависит от настроек. Привилегированный режим, общие каталоги и доступ к сокету управления Docker могут дать заданию возможности за пределами контейнера. В рекомендациях по защите раннеров GitLab отдельно рассматриваются риски общих машин, повышенных привилегий и сетевого доступа. Практический ориентир для чувствительных сборок предполагает отдельное чистое окружение на одно задание. После работы следует уничтожать среду, а не только каталог проекта. При этом одноразовый раннер не исправляет заражённый базовый образ и не обезвреживает вредоносную зависимость. До завершения задания чужой код успеет воспользоваться всеми выданными правами. Разделять нужно и сеть. Раннеру тестов обычно незачем обращаться к рабочей базе или административным интерфейсам. Ограничение исходящих соединений сокращает возможности утечки, но требует аккуратной настройки. Разрешённый сервис тоже может стать каналом передачи данных, если атакующий способен записывать туда файлы или публиковать результаты. ## Секрет в переменной окружения остаётся доступным коду Секреты в настройках CI защищают от прямого попадания паролей в репозиторий. Однако в момент работы конвейер должен передать секрет процессу, который подключается к сервису. Если переменная окружения доступна вредоносному процессу, скрытое поле в интерфейсе уже не помогает. Существенен фактический доступ во время выполнения. Маскирование журналов решает более узкую задачу. Система старается скрыть известное значение в выводе команд, но не запрещает процессу прочитать секрет, сохранить в файл или передать по сети. Преобразованное значение также может не совпасть с шаблоном маскирования. Проверка журналов нужна, но считать звёздочки в интерфейсе защитой от вредоносного задания нельзя. В инциденте Codecov 2021 года злоумышленник менял Bash Uploader, вспомогательный сценарий для отправки отчётов о покрытии кода тестами. Несанкционированные изменения происходили начиная с 31 января, проблему обнаружили 1 апреля. Подменённый сценарий отправлял переменные окружения и сведения о репозитории на посторонний сервер. Возможный ущерб зависел от того, какие секреты окружение предоставляло сценарию. Показательная деталь связана с первоначальным доступом. Ошибка в процессе создания Docker-образа Codecov позволила извлечь учётные данные для изменения загрузчика. Секрет одного поставщика открыл путь к инструменту, а инструмент получил доступ к секретам клиентов. Само приложение клиента при этом могло вообще не меняться. Первое исправление состоит в сокращении доступа. Отправителю отчёта нужен токен отправки отчёта, сборке нужен доступ к необходимым зависимостям, публикации нужны права на конкретный пакет. Секрет рабочего окружения следует выдавать только отдельному заданию развёртывания. Передать секрет «лишь на последнем шаге» внутри уже скомпрометированного задания недостаточно, поскольку предыдущий шаг мог оставить работающий процесс или изменить файлы. Для поддерживающих такой режим сервисов полезны краткоживущие учётные данные через OpenID Connect, или OIDC. Система CI подтверждает сведения о задании, а внешний сервис выдаёт ограниченные полномочия. Доверие нужно привязать к разрешённому проекту и контексту выпуска. Короткий срок действия сокращает окно злоупотребления, но не мешает атакующему воспользоваться правами, пока скомпрометированное задание работает. ## Вспомогательный шаг тоже входит в цепочку поставок При проверке зависимостей легко сосредоточиться на пакетах приложения и пропустить компоненты самого конвейера. Инструмент определения изменённых файлов выглядит безобиднее библиотеки авторизации. Однако оба компонента исполняют код, а реальный риск определяется доступными правами. В марте 2025 года атакующие скомпрометировали tj-actions/changed-files, компонент GitHub Actions для определения изменённых файлов. Злоумышленники перенаправили теги версий на вредоносный код, который извлекал секреты из памяти процесса раннера и выводил в журналы. В публичных репозиториях журналы могли открыть секреты посторонним. Для закрытых репозиториев риск зависел в том числе от доступа к журналам. Урок касается привычной записи версии. Тег в Git служит именем ссылки на состояние репозитория, а при наличии прав ссылку можно перенести. Фиксация внешнего компонента на полном идентификаторе проверенного коммита защищает от такого перенаправления. Однако сам выбранный коммит тоже нужно оценить, а загружаемые им дополнительные файлы требуют отдельного контроля. Поэтому инвентаризация конвейера должна охватывать внешние действия, подключаемые сценарии, базовые образы и удалённые загрузчики. Для каждого компонента полезно знать владельца, точную версию, разрешённые сетевые обращения и права задания. Подключение ещё одного сканера безопасности тоже добавляет исполняемый компонент, которому следует назначить минимальные полномочия. ## SolarWinds показала разрыв между исходниками и выпуском Проверка репозитория отвечает на вопрос о содержимом репозитория. Пользователь получает другой объект, готовую программу. Между двумя объектами работают компиляторы, сценарии и сборочная инфраструктура. Если атакующий контролирует промежуточный этап, просмотр изменений разработчиками может не обнаружить подмену. В атаке на SolarWinds Orion, раскрытой в декабре 2020 года, вредоносный инструмент SUNSPOT следил за процессом сборки. При подходящих условиях инструмент временно заменял исходный файл версией с закладкой SUNBURST, а после завершения возвращал оригинал. Заражённый компонент попадал в штатный выпуск. Вредоносные обновления распространялись с цифровой подписью SolarWinds. Подпись здесь не была бесполезной, просто от неё ожидали лишнего. Проверка подписи подтверждает связь файла с подписавшей стороной и позволяет обнаружить последующее изменение. Но подпись не доказывает отсутствие вредоносной логики и не гарантирует, что файл собрали именно из проверенных исходников. Если заражённый результат попал в разрешённую процедуру подписи, криптография исправно подтвердит заражённый результат. Для защиты нужна проверяемая связь между исходным кодом, средой сборки и конкретным файлом. Сведения о происхождении артефакта фиксируют, откуда взяли код, каким процессом выполнили сборку и к какому результату относится запись. Подход описан в требованиях SLSA к проверке артефактов. Проверяющая сторона должна сопоставлять сведения со своими ожиданиями, а не принимать любую корректно подписанную запись. Хранить ключ подписи и создавать все доказательства внутри того же потенциально захваченного задания недостаточно. Нужен защищённый механизм, которому сборочный код не может произвольно диктовать результат проверки. Для подходящих проектов дополнительную уверенность даёт независимая воспроизводимая сборка. Совпадение результатов помогает выявлять подмену на отдельной машине, но не обнаружит вредоносный код, уже включённый в общие исходники. ## Кеш и реестр могут подменить уже проверенный результат Атакующему необязательно присутствовать в момент публикации. Иногда достаточно заранее изменить файл, который доверенное задание возьмёт из общего хранилища. Кеш ускоряет сборку, но право записывать в кеш, используемый выпуском, фактически даёт влияние на будущий выпуск. Аналогичный риск возникает при передаче артефактов между процессами с разными полномочиями. Привилегированное задание не должно считать файл доверенным только потому, что файл называется «результат тестов» или появился внутри той же платформы CI. Следует проверять, какой запуск создал файл, из какого исходного состояния и при каких условиях. На этапе развёртывания опасны изменяемые указатели вроде `latest`. Команда могла проверить один контейнерный образ, а позже получить по тому же тегу другой. Надёжнее передавать между этапами точный идентификатор содержимого, криптографический хеш, и развёртывать проверенный объект. Повторная сборка после тестов создаёт новый объект, который требует собственных проверок. Для рабочих процессов полезны разделённые кеши, ограничения перезаписи выпусков и отдельные права на загрузку и развёртывание. Хеш нужно получать из доверенного источника. Если атакующий заменяет одновременно файл и лежащую рядом контрольную сумму, сравнение подтвердит только согласованность двух подменённых объектов. ## Как проверить свой конвейер по карте угроз Начать лучше с одного продукта и одного пути выпуска. Выберите обновление, которое команда действительно отправляет пользователям, и проследите все переходы от изменения в репозитории до установки. Для каждого перехода запишите, кто может изменить входные данные, какой код запускается и какие права получает процесс. Затем проверьте границы на безопасном стенде. Вместо настоящих секретов используйте контрольные значения, а действия ограничьте тестовыми ресурсами. Цель состоит в проверке конкретного утверждения, например «внешнее изменение не может получить полномочия публикации». Сам факт успешной сборки такого утверждения не подтверждает. — Проверьте, какие изменения запускают задания до одобрения, включая тесты, сборочные файлы и установочные сценарии. — Сопоставьте каждое задание с доступными секретами, служебными правами, каталогами и сетевыми ресурсами. — Проверьте, остаются ли после задания файлы или процессы, способные повлиять на следующий запуск. — Установите, могут ли недоверенные задания записывать в кеши и хранилища, которые использует выпуск. — Сравните идентификатор проверенного артефакта с идентификатором реально опубликованного или развёрнутого файла. — Проверьте, кто может изменить правила конвейера, выдать дополнительные права или обойти обязательное одобрение. Порядок исправлений стоит выбирать по последствиям. В первую очередь разрывать путь от внешнего кода к рабочей инфраструктуре, затем ограничивать права публикации и общие среды, после чего усиливать контроль компонентов и происхождения сборок. Защита конфигурации CI столь же существенна, как защита исходников. Если один захваченный аккаунт может убрать все проверки, остальные меры становятся необязательными. При обнаружении заражённой зависимости или раннера остановки сборки недостаточно. Нужно изолировать подозрительную среду, сохранить доступные следы, отозвать потенциально раскрытые учётные данные и проверить действия с их использованием. Выпуски за затронутый период требуют отдельной оценки, а доверенные артефакты следует пересобрать в чистом окружении. Удаление пакета не возвращает украденный токен и не отменяет уже опубликованное обновление. Хорошая карта угроз позволяет ответить на практический вопрос. Если один компонент конвейера окажется вредоносным, на каком переходе атака остановится? Ответ должен указывать на конкретное ограничение прав, изолированную среду или обязательную проверку артефакта. Зелёная отметка возле завершённой сборки сообщает только о том, что заданные команды закончились успешно.
Популярное:
- Идеальный код с трояном внутри: как доверенный конвейер CI/CD превращается в оружие против клиентов
- В Аромашевском округе Тюменской области жители узнали о семейных фермах
- Слепые зоны SAST, DAST и SCA: как формальные проверки создают опасную иллюзию защищенности сервиса
- Определен самый некурящий регион России
- Дмитрий Чернышенко: Россия — союз почти 200 национальностей и народов
- Reuters: крупнейшая авиакомпания Латвии AirBaltic начала процедуру банкротства
- Секреты, контейнеры и IaC. Где CI/CD оставляет лишний доступ
- В Омске оштрафовали водителя BMW за попытку подкупа полицейского
