open atlas
↑ К треку
CI/CD-пайплайны CICD · 08 · 01

События и триггеры: что запускает workflow и ловушка pull_request_target

Workflow управляется событиями: блок `on:`, фильтры и concurrency решают, что запускается и что отменяется. pull_request выполняет недоверенный код без секретов; pull_request_target — доверенный код с секретами поверх форка — классическая дыра утечки.

CICD Senior ◷ 16 min
Уровень
ОсновыJuniorMiddleSenior

Пост-мортем утечки читался как фокус. Случайный контрибьютор открыл pull request из форка, тронув только тестовый конфиг — ничего подозрительного. CI отработал, стал зелёным, и через двадцать минут NPM_TOKEN репозитория и deploy-ключ оказались на pastebin. Никто ничего не мержил. Фокус был в триггере: кто-то месяцами ранее переключил lint-workflow с pull_request на pull_request_target, чтобы «починить» жалобу о недоступности секретов. Это одно слово полностью сменило модель безопасности — pull_request_target выполняет определение workflow из базовой ветки, но с выгруженным кодом форка, и, главное, с полным доступом к секретам репозитория. Вредоносный pretest-скрипт форка выполнился с токеном в окружении. Лечением был не патч, а понимание: выбранное событие решает, встретится ли управляемый атакующим код с вашими секретами.

Блок on: — это вся точка входа

Workflow не делает ничего, пока событие не совпадёт с его блоком on:. Есть три широких семейства событий, и различие управляет и поведением, и безопасностью:

on:
  push:
    branches: [main, "release/**"]
    paths-ignore: ["docs/**"]
  pull_request:
    types: [opened, synchronize, reopened]
  schedule:
    - cron: "17 3 * * 1-5"   # 03:17 UTC, пн–пт — смещение, чтобы уйти от давки на верху часа
  workflow_dispatch:          # вручную, с типизированными inputs
  workflow_call:              # вызывается как reusable workflow из другого

Фильтры комбинируются AND между ключами, OR внутри списка: branches: [main] и paths: [src/**] оба должны выполниться, но любая ветка из списка подходит. paths и paths-ignore взаимоисключающи на одном событии; смешение положительных и отрицательных глобов в одном списке paths — обычный источник «почему мой workflow не запустился». Тонкость, которая кусает команды с required-проверками: если фильтр paths пропускает workflow, required-проверка отчитывается как skipped, а не passed — и правило защиты ветки, требующее её, заблокирует мерж навсегда, пока вы не сделаете проверку сквозной. schedule cron только в UTC и работает по best-effort (по возможности, без гарантии): GitHub явно отбрасывает запланированные запуски под нагрузкой, и запуски на верху часа отбрасываются и задерживаются чаще всего — потому продакшн-расписания используют смещение вроде 17 3.

pull_request против pull_request_target — развилка безопасности

Для PR из форка pull_request выполняет workflow из head-ref PR, в песочнице с read-only GITHUB_TOKEN и без доступа к секретам. Это безопасный дефолт: недоверенный код выполняется, но не дотянется ни до чего ценного. pull_request_target выполняет workflow из базовой ветки (то есть файл workflow доверенный), но в контексте базового репозитория с полными секретами и read-write токеном — при этом код форка — это то, что выгрузится, если наивно сделать actions/checkout head PR. Эта комбинация и есть вектор эксфильтрации из Hook: доверенный триггер, недоверенный код, живые секреты.

# ОПАСНО: секреты в зоне видимости, затем выгружается и выполняется код форка
on: pull_request_target
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}  # ← код атакующего
      - run: npm ci && npm test   # ← выполняется с NPM_TOKEN в env

Эвристика: используй pull_request_target только для доверенной работы, которая никогда не выполняет код форка (повесить label, оставить комментарий, прогнать проверку, читающую метаданные). В момент, когда нужно собрать или протестировать реальный код контрибьютора, используй pull_request и прими, что секреты недоступны — либо закрой привилегированный шаг за environment с ручным одобрением.

Викторина

Почему workflow по pull_request_target, выгружающий и тестирующий head-код PR, — дыра в безопасности, а те же шаги под pull_request безопасны?

Почему это работает

Зачем GitHub вообще предлагает pull_request_target? Потому что легитимным PR из форков нужна какая-то доверенная автоматизация: автолейблинг, боты «needs-review», публикация комментариев о покрытии — работа, которая должна читать или писать состояние репозитория, но не должна выполнять код контрибьютора. Событие существует, чтобы доверенная работа с метаданными имела учётки, пока путь недоверенного кода (pull_request) остаётся заперт. Провал — слить эти два: делать выполнение недоверенного кода под доверенным триггером. Держи их раздельно — и каждый безопасен.

Concurrency: отмени устаревший запуск, пока он не сжёг раннер

Каждая оживлённая PR-ветка теряет минуты раннеров на запуски, которые будут обогнаны до окончания — и без политики concurrency ты платишь за каждый. Спроси себя: есть ли что-то полезное в прогоне CI-сьюта против коммита, обогнанного сорок секунд назад?

Когда контрибьютор пушит три коммита за пять минут, три запуска становятся в очередь. Без управления concurrency все три выполнятся, а первые два устаревают в момент старта третьего. Блок concurrency это чинит:

concurrency:
  group: ci-${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

cancel-in-progress: true отменяет любой выполняющийся запуск той же группы при старте нового — канонический PR-паттерн, с ключом по github.ref, чтобы каждая ветка была своей группой. Это регулярно срезает большую долю впустую потраченных минут на оживлённых ветках. Обратный паттерн тоже важен: для deploy-workflow нужен cancel-in-progress: false, чтобы деплой никогда не убивался на полпути, а группа сериализовала деплои по одному за раз. Когда добавляешь concurrency к deploy-workflow и второй деплой стартует на полпути первого — сначала реши, какой провал хуже: наполовину задеплоенное окружение или деплой в очереди, который просто ждёт. Concurrency — это гейт очереди, а не триггер: событие всё равно срабатывает; concurrency лишь решает, какие запуски выживут.

Викторина

Deploy-в-прод workflow использует concurrency с cancel-in-progress: true по ключу environment. Второй деплой стартует, пока первый на полпути. Что происходит и того ли вы хотели?

Вспомните перед уходом
  1. 01
    Объясни точную разницу между pull_request и pull_request_target и почему один — известный вектор эксфильтрации.
  2. 02
    Как комбинируются фильтры on:, что делает schedule ненадёжным и почему фильтр путей ломает мерж?
Итог

Workflow GitHub Actions управляется событиями с первой строки: блок on: называет события — push, pull_request, schedule, workflow_dispatch, workflow_call — и его фильтры комбинируются AND между ключами и OR внутри списка, с взаимоисключающими paths/paths-ignore и пропущенной по paths required-проверкой, блокирующей мержи как «skipped», а не «passed». schedule только в UTC и best-effort, поэтому GitHub отбрасывает запуски под нагрузкой, а джобы на верху часа — самые вероятные жертвы; смещай минуту. Модель безопасности решается на триггере, а не в джобах: pull_request выполняет недоверенный код форка с read-only токеном и без секретов, поэтому его выполнение безопасно; pull_request_target выполняет доверенный workflow базовой ветки с полными секретами и write-токеном, что становится дырой эксфильтрации в момент, когда вы выгружаете и запускаете head-код форка — резервируй его для доверенной работы с метаданными, а выполнение недоверенного кода переноси на pull_request или за environment с ручным одобрением. Concurrency — гейт очереди поверх триггера: cancel-in-progress: true по ключу github.ref убивает устаревшие PR-запуски и возвращает минуты, а деплои переключают его на false, чтобы группа сериализовала и ни один деплой не умер на полпути. Выбирай событие осознанно — оно определяет, что запустится, что отменится и встретится ли код атакующего с твоими секретами.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 5 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.