ADG Оставить заявку
Блог DevOps 4 мин чтения

GitHub Actions и утечка токенов: аудит CI/CD цепочки поставки

Инцидент Heroku/Salesforce с кражей OAuth-токенов через GitHub Actions заставил нас прочесать CI/CD пайплайны клиентов: permissions, OIDC, форки - разбираем что нашли.

Контекст момента

Апрель 2022: Heroku/Salesforce подтверждают кражу OAuth-токенов через GitHub Actions; началась серия инцидентов с утечкой секретов в CI/CD цепочках поставки

В начале апреля Heroku и Salesforce подтвердили то, о чём ходили слухи уже несколько недель: злоумышленники получили OAuth-токены интеграции GitHub, воспользовавшись доступом через GitHub Actions. Детали инцидента ещё выясняются, но основная механика уже понятна - скомпрометированный токен с избыточными правами, долгий срок жизни, и цепочка поставки как вектор атаки.

Мы занимались аналогичными разборами с Log4Shell - там реестр зависимостей оказался ключевым инструментом. Здесь другой класс проблем: не уязвимости в библиотеках, а конфигурация самих пайплайнов. Решили не ждать, когда клиент сам придёт с вопросом, и прошлись по CI/CD активно.

Что ищем

Аудит CI/CD-пайплайнов фокусировался на трёх вещах.

Первое - permissions в workflow-файлах. По умолчанию GitHub Actions выдаёт токену GITHUB_TOKEN права write на всё - contents, packages, pull-requests, deployments. Большинству джобов это не нужно. Если в workflow нет явного блока permissions, токен получает максимум. Мы смотрели на каждый .github/workflows/*.yml и выставляли permissions: read-all на уровне workflow с переопределением для конкретных джобов, которым нужно что-то записать. Принцип минимальных привилегий - ничего нового, но в CI/CD он соблюдается хуже, чем в production-конфигурациях.

Второе - долгоживущие токены в секретах. Классическая схема: создали персональный токен или сервисный аккаунт, положили в Secrets, забыли. Токен живёт год или вообще бессрочно, попасть к нему можно через взломанный workflow, через форк с вредоносным PR, через компрометацию одного из action-зависимостей. OIDC (OpenID Connect) решает это принципиально: облачный провайдер выдаёт краткоживущие credentials напрямую джобе по запросу, никакого статичного секрета не существует. GitHub Actions поддерживает OIDC для AWS, GCP, Azure. Там, где клиенты деплоили в эти облака с долгоживущими ключами, переходили на OIDC. Занимает пару часов на один пайплайн, разница в профиле риска существенная.

Третье - триггер pull_request от форков. Репозитории с открытыми PR из форков - отдельная история. Когда джоба запускается на pull_request от внешнего форка, по умолчанию в ней нет доступа к секретам репозитория - GitHub это ограничивает. Но если где-то стоит pull_request_target вместо pull_request, или явно прописан secrets: inherit для форков - это дыра. Нашли несколько таких конфигураций у клиентов с публичными репозиториями. Отдельно: workflow_run как триггер с pull_request_target - популярная схема, которая выглядит безопасно но таковой не является, если не понимаешь что делаешь.

Что нашли

Картина не катастрофическая, но показательная. Ни один из клиентов не настраивал permissions явно - везде стоял дефолт с write-доступом. Долгоживущие токены для деплоя в AWS нашли в половине пайплайнов, которые деплоят в облако. OIDC не использовал никто - не потому что против, а потому что не знали о такой опции или не дошли руки разобраться.

Форковые сценарии с pull_request_target нашли в двух репозиториях - оба публичные, оба с CI который запускает линтер и тесты на PR. Конфигурация досталась от шаблона и никто особо не читал что там написано.

Ни одного подтверждённого инцидента у наших клиентов нет. Но ценность аудита именно в том, чтобы находить такие вещи до инцидента, а не разбирать почему токен утёк.

Что поправили

Типичный пайплайн после аудита стал выглядеть примерно так:

permissions:
  contents: read

jobs:
  build:
    permissions:
      contents: read
      packages: write   # только если нужен push в registry
    steps:
      - uses: actions/checkout@v3

Для деплоя в AWS заменили AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY в секретах на OIDC:

- uses: aws-actions/configure-aws-credentials@v1
  with:
    role-to-assume: arn:aws:iam::123456789:role/github-actions-deploy
    aws-region: us-east-1

IAM-роль с AssumeRoleWithWebIdentity - и никакого долгоживущего ключа в репозитории.

Форковые пайплайны переписали с pull_request_target на pull_request - те, кому это позволяла логика. Где без pull_request_target не обойтись, добавили явные условия на github.event.pull_request.head.repo.full_name == github.repository, чтобы джоба с доступом к секретам не запускалась из форка.

Где находимся

Инцидент с Heroku ещё расследуется, полный разбор атаки на цепочку поставки не опубликован. Но базовые меры не требуют ждать постмортема - конфигурация пайплайнов с минимальными правами и без долгоживущих секретов полезна независимо от конкретного вектора атаки.

Аудит CI/CD как отдельная процедура - не то, о чём клиенты спрашивают сами. Обычно это вытаскивается как часть общего аудита безопасности инфраструктуры. После этой недели думаем сделать из него отдельный чеклист и проходиться по нему при онбординге новых клиентов.

Контакт

Нужна такая же инженерная работа?

Опишите задачу и контекст. Ответим в течение рабочего дня, при необходимости подпишем NDA.