Безопасность CI/CD runner: как атакуют self-hosted агенты и что с этим делать

В сентябре 2025 года кампания GhostAction скомпрометировала 327 GitHub-аккаунтов: из 817 репозиториев утекло 3325 секретов — облачные ключи AWS, токены публикации npm, пароли Docker Hub. По данным Elastic Security Labs, весь вектор атаки свёлся к модификации одного YAML-файла в .github/workflows/. Через два месяца червь Shai-Hulud пошёл дальше — превращал self-hosted runner’ы в бэкдоры, используя GitHub Discussions как C2-канал через легитимный HTTPS-трафик. Ниже — разбор того, как именно ломают CI/CD runner’ы, и конкретные шаги, которые закрывают эти дыры до появления атакующего.
Почему self-hosted runner — главная мишень в CI/CD пайплайне
CI/CD-пайплайн (Continuous Integration / Continuous Delivery — автоматическая сборка, тестирование и доставка кода в продакшн) по природе своей — привилегированная инфраструктура. Runner — машина (физический сервер, виртуалка или контейнер), которая выполняет задачи пайплайна: компилирует код, прогоняет тесты, деплоит. GitHub и GitLab предлагают два варианта: managed runner’ы (платформа сама поднимает и уничтожает VM под каждую задачу) и self-hosted runner’ы (агент стоит на твоём собственном сервере).
Self-hosted выбирают по конкретным причинам: compliance-требования (финтех, госсектор — код не должен покидать периметр), нестандартное железо (embedded, IoT) или банальная экономия на облаке. По данным Legit Security, из 3,1 миллиона проверенных публичных GitHub-репозиториев 43 803 используют self-hosted runner’ы. На первый взгляд — полтора процента. Но каждый такой runner выполняет произвольный код by design.
Вот что получает атакующий при компрометации self-hosted runner’а:
- Секреты пайплайна — AWS-ключи, OAuth-токены, приватные ключи подписи, OIDC-токены. Всё, что workflow передаёт в переменные окружения.
- Доступ к внутренней сети. Runner часто стоит внутри VPC или корпоративного периметра. Один скомпрометированный агент — и атакующий начинает lateral movement (горизонтальное перемещение по сети, когда из одной машины «прыгают» на соседние).
- Кешированные артефакты. Если runner не эфемерный (не уничтожается после каждой задачи), в его рабочих директориях лежат исходники, билды и конфиги от предыдущих проектов.
- Эскалация до supply chain атаки — когда вредоносный код попадает к конечным пользователям через доверенный канал обновлений. Контроль над runner’ом = контроль над тем, что попадает в релизный артефакт.
Монетизация конкретна: украденные облачные ключи превращаются в крипто-майнинг (один угнанный AWS-аккаунт — это счета на десятки тысяч долларов за вычислительные ресурсы), а украденные токены npm или PyPI позволяют отравить downstream-зависимости и поразить тысячи проектов ниже по цепочке.
Реальные атаки на CI/CD runner: три вектора 2025-2026
В базе MITRE ATT&CK (открытая база тактик и техник реальных атакующих; каждой технике присвоен T-код — например, T1552 означает «Unsecured Credentials») атаки на CI/CD покрывают сразу несколько тактик. Три вектора, которые активно эксплуатировались в 2025-2026.
Инъекция в workflow и кража секретов из пайплайна
Цепочка: украденные учётные данные разработчика → модифицированный workflow-файл (YAML, описывающий шаги CI/CD) → все секреты CI-окружения улетают на эндпоинт атакующего.
GhostAction показала это в масштабе. Атакующий дописывал в workflow-файлы пару строк — POST-запрос, отправляющий переменные окружения на внешний сервер. Elastic Security Labs зафиксировали итог: 327 скомпрометированных пользователей, 817 репозиториев, 3325 секретов. Среди украденного — облачные ключи, токены публикации npm и Docker Hub, GITHUB_TOKEN с правами записи.
Червь Shai-Hulud добавил к этому самораспространение. Он вытягивал GitHub Personal Access Token через gh auth token, запускал TruffleHog (инструмент, который ищет секреты в коде — ключи, пароли, токены) для разведки и использовал найденные токены для инъекции вредоносного кода в пакеты того же разработчика. В первой волне — более 46 000 вредоносных npm-пакетов (данные Elastic Security Labs).
Runner как persistent бэкдор — supply chain атака через DevOps
Shai-Hulud (ноябрь 2025, задокументирован Sysdig) показал, что self-hosted runner превращается в полноценный бэкдор, работающий целиком через легитимные каналы GitHub. Это техника T1195 (Supply Chain Compromise) из тактики Initial Access в MITRE ATT&CK.
Атака идёт в четыре стадии:
-
Создание репозитория. Обнаружив валидный GitHub-токен, червь создаёт публичный репозиторий с включённым Discussions.
-
Получение registration token. Через GitHub API запрашивается токен регистрации runner’а. Для этого нужны admin-права к репозиторию — они уже есть благодаря украденному токену.
-
Установка runner’а. Официальный бинарник GitHub Actions Runner скачивается в скрытую директорию
~/.dev-env/, конфигурируется с флагомRUNNER_ALLOW_RUNASROOT=1(обход штатной защиты — по умолчанию runner отказывается запускаться от root) и стартует в фоне черезnohup. Процесс переживает завершение родительского скрипта. -
Заражённый workflow с command injection. В репозиторий загружается
discussion.yaml, где конструкция${{ github.event.discussion.body }}подставляется прямо вrun:-блок. Любой текст в Discussion выполняется как shell-команда на машине жертвы. GitHub подставляет содержимое выражения в скрипт до его запуска — это не баг, а документированное поведение expression interpolation. Неочевидный момент для тех, кто только начинает работать с Actions: выражения${{ }}раскрываются до запуска шелла, как макросы — поэтому любой пользовательский ввод в них становится кодом.
Весь трафик идёт на github.com. IDS и файрволы его не фильтруют — легитимный HTTPS к доверенному домену.
Отравление pull_request_target: атаки через ненадёжные форки
Триггер pull_request_target в GitHub Actions — одна из самых опасных функций. В отличие от обычного pull_request, он выполняет workflow в контексте целевого репозитория с доступом ко всем секретам, но может запускать код из ненадёжного форка (копии репозитория, созданной внешним пользователем). Проще говоря: кто угодно делает форк, отправляет PR — и его код выполняется с правами и секретами оригинального проекта.
В начале 2026 года были зафиксированы кампании, целенаправленно сканировавшие публичные репозитории на эту ошибку конфигурации. Типичный арсенал: заражённые Go-функции init(), command injection через имя ветки, инъекция через имя файла, прямая скриптовая инъекция и даже AI prompt injection против автоматизированных code reviewer’ов. В тяжёлых сценариях такая атака приводит к полной компрометации репозитория популярного инструмента и downstream supply chain атаке с массовой утечкой секретов.
Отдельная техника из тактики Defense Evasion — T1070.006 (Timestomp): атакующие задним числом меняли метки времени коммитов, чтобы вредоносные файлы выглядели «старыми и доверенными». При ревью коммит с датой годовой давности не вызывает подозрений — на это и расчёт.
Защита self-hosted runner: делай раз, делай два, делай три
Эфемерные runner’ы — уничтожай агент после каждой задачи
Зачем: если runner уничтожается после каждого задания, атакующий не может закрепиться на машине, а артефакты предыдущих сборок не остаются на диске. Это главная линия обороны от всех трёх описанных выше атак.
GitHub Actions. При регистрации self-hosted runner’а добавь флаг --ephemeral. Предпосылка: машина с установленным runner’ом, токен регистрации из Settings → Actions → Runners.
./config.sh --url https://github.com/<ORG>/<REPO> \
--token TOKEN \
--ephemeral \
--disableupdate
Runner примет одну задачу и автоматически снимет свою регистрацию. Для следующей задачи нужен новый инстанс. Флаг --disableupdate запрещает авто-обновление — контроль версии остаётся за тобой.
GitLab CI. В config.toml (конфиг runner’а) установи MaxBuilds = 1. Предпосылка: установленный GitLab Runner, доступ к /etc/gitlab-runner/config.toml.
[[runners]]
limit = 1
[runners.machine]
MaxBuilds = 1
После одной сборки runner уничтожает рабочее окружение. Документация GitLab рекомендует это для всех shared runner’ов, особенно при --privileged режиме Docker.
Kubernetes. Kubernetes executor для GitLab или ARC (Actions Runner Controller) для GitHub создают под каждую задачу отдельный pod (минимальная единица запуска в Kubernetes — изолированная среда с собственной файловой системой и сетью). После завершения pod удаляется со всем содержимым.
Как проверить: после выполнения pipeline загляни в Settings → CI/CD → Runners. Отработавший runner должен исчезнуть из списка активных. Если висит — конфигурация неверна, и у тебя не эфемерный runner, а обычный.
Изоляция CI/CD runner по уровню доверия
Зачем: один runner, обслуживающий и dev-, и prod-окружения, позволяет job’у из тестовой ветки получить доступ к production-секретам.
| Мера | GitHub Actions | GitLab CI |
|---|---|---|
| Группы runner’ов | Runner groups в Settings — Actions | Runner tags + protected flag |
| Запрет на публичные репо | Disallow public repositories | Нерелевантно для self-managed |
| Выбор executor’а | Runner = процесс на хосте | Docker executor вместо Shell |
| Ограничение по веткам | Environment protection rules | Protected runners (only protected branches) |
Shell executor vs Docker executor — критический выбор в GitLab, и тут стоит разобраться подробнее. Shell executor запускает команды job’а прямо на хосте, с правами пользователя gitlab-runner. Вредоносная задача может прочитать файлы других проектов, установить malware или получить доступ к сети. Docker executor запускает каждую задачу в изолированном контейнере. Не абсолютная защита (при --privileged изоляция ломается — контейнер получает доступ к устройствам хоста), но поверхность атаки значительно уже.
Исследование risk3sixty показало эту разницу на практике: обнаружив instance runner с shell executor на GitLab, пентестер через простой .gitlab-ci.yml с реверс-шеллом получил полный доступ к хосту, затем через AWS metadata service (доступен по адресу 169.254.169.254 изнутри EC2) — к IAM-роли с правами SSM, и далее к root-доступу на других EC2-инстансах во всей VPC. Один runner с shell executor стал точкой входа во всю облачную инфраструктуру.
Чек-лист: никогда не используй shell executor на shared runner’ах. Не разрешай self-hosted runner’ы для публичных репозиториев. Разделяй runner’ы для dev/staging/prod через runner groups и теги. Это не «рекомендации» — это минимум, без которого runner торчит наружу.
OIDC вместо статических секретов в пайплайне
Зачем: статические ключи (AWS Access Key, GCP Service Account JSON) в переменных окружения CI — долгоживущие секреты. Утечка одного ключа = постоянный доступ к облаку до момента ручной ротации. А ротируют их, будем честны, нечасто.
OIDC-федерация (OpenID Connect — протокол, позволяющий CI-системе подтвердить свою идентичность облачному провайдеру без передачи статического секрета) решает эту проблему. GitHub Actions или GitLab CI выступает поставщиком идентичности, а облачный провайдер (AWS, GCP, Azure) выдаёт короткоживущие временные ключи под конкретный запуск pipeline.
Для GitHub Actions: в workflow добавляется permissions: id-token: write, вместо статических ключей используется action aws-actions/configure-aws-credentials@v4 с параметром role-to-assume. На стороне AWS создаётся IAM-роль с trust policy, привязанной к конкретному репозиторию и ветке (паттерн repo:MY-ORG/MY-REPO:environment:production). Время жизни STS-сессии — от 15 минут до 12 часов (по умолчанию 1 час).
Для GitLab CI: начиная с версии 15.7 используется ключевое слово id_tokens (старый CI_JOB_JWT deprecated в 15.9, удалён в 17.0 — если ты его ещё используешь, пора мигрировать). Каждый job может запросить токен с указанием конкретной аудитории — например, aud: https://sts.amazonaws.com.
Результат: даже если workflow скомпрометирован, атакующий получает ключ, действительный максимум час и привязанный к конкретному репозиторию. Вместо бессрочного доступа ко всем ресурсам аккаунта.
Как обнаружить атаку на CI/CD пайплайн
Для автоматизации есть open-source инструменты анализа CI/CD-конфигураций — drop-in шаблоны для GitHub Actions, GitLab CI и Azure DevOps, которые анализируют дифф каждого pull request на подозрительные изменения.
Но даже без автоматизации — при ручном ревью CI-конфигов — есть набор сигналов, которые должны вызывать немедленную реакцию:
| Сигнал | Что означает | Уровень риска |
|---|---|---|
${{ github.event.*.body }} в run: |
Command injection через неконтролируемый ввод | Критический |
permissions: write-all |
Избыточные права GITHUB_TOKEN | Высокий |
runs-on: self-hosted в публичном репо |
Доступ к внутренней инфраструктуре из интернета | Критический |
Ссылка action@main вместо action@SHA |
Mutable reference — подмена action без следов | Высокий |
curl URL \| bash в build-шаге |
Выполнение неверифицированного кода | Высокий |
RUNNER_ALLOW_RUNASROOT=1 |
Обход защиты от запуска с root-правами | Критический |
Для мониторинга инфраструктуры self-hosted runner’ов: отслеживай создание процессов в нетипичных директориях (паттерн ~/.dev-env/ из Shai-Hulud), появление runner-процессов с RUNNER_ALLOW_RUNASROOT=1, нетипичные API-вызовы к GitHub (создание репозиториев, запрос registration token) от имени сервисных учёток. В репозитории SigmaHQ есть набор правил по тегу T1195 (Supply Chain Compromise) — 19 правил, включая обнаружение аномальной активности в файловой системе и DNS-запросов к нехарактерным доменам. Sigma — это формат описания детектирующих правил, который конвертируется в запросы для конкретных SIEM (Splunk, Elastic, KUMA).
Три кампании — GhostAction, Shai-Hulud, HackerBot-Claw — объединяет один паттерн: атакующие не ломают продакшн-серверы напрямую. Они ломают автоматизацию, которая деплоит на эти серверы. Один скомпрометированный workflow-файл даёт больше, чем RCE на отдельном хосте: доступ ко всей цепочке поставок ПО.
Большинство команд, внедряющих DevSecOps, фокусируются на SAST (статический анализ кода), SCA (анализ зависимостей) и сканировании контейнеров — и полностью игнорируют безопасность самих runner’ов. Парадокс: Trivy, Semgrep, Gitleaks встроены в пайплайн, но агент, исполняющий эти проверки, работает на non-ephemeral VM с shell executor, доступном из любого репозитория организации. Замок на двери при открытом окне.
По опыту настройки пайплайнов в компаниях с self-hosted инфраструктурой — первое, что стоит проверять при аудите, это конфигурация runner’ов, а не набор сканеров. Эфемерные runner’ы в Kubernetes с минимальными правами сервис-аккаунта, OIDC-федерация вместо статических ключей, runner groups с разделением dev/staging/prod — не «best practices из документации», а базовый гигиенический минимум, без которого любой SAST-сканер бесполезен. Атакующий просто модифицирует workflow — и сканер пропустит что угодно.
Пока CI/CD-инфраструктуру не начнут воспринимать как привилегированный актив уровня domain controller, инциденты масштаба GhostAction будут повторяться. Если ты в начале DevSecOps-карьеры и хочешь выстроить понимание этих процессов системно — IB Basics на codeby.school закрывает именно этот пробел: не теория из учебников, а задачи, с которыми сталкиваешься на первом рабочем месте.
Эту тему и смежные навыки разбирают на практике в курсе «Основы практик DevOps» Codeby Academy.
