Learn (Almost) Anything
КаталогСкачать курс

Автоматизация в GitHub

Модель угроз GitHub Actions: untrusted code, secrets exposure, token permissions, script injection, third-party actions, logs и принцип least privilege.

ruЭнциклопедия33 урока6 модулей9
GitHub actionsАвтоматизация в GitHubМодель workflowсостояние и артефактыматрицы и runtimeрелизы и environmentsПереиспользование и собственные actionsgovernance и эксплуатацияgithubactions

Secure use и threat model

Модель угроз GitHub Actions: untrusted code, secrets exposure, token permissions, script injection, third-party actions, logs и принцип least privilege.

Secure use и threat model

Secure use в GitHub Actions начинается не с галочки «включить security», а с модели угроз: что именно может выполнить чужой код, какие credentials доступны job, какие actions вы импортируете и что попадёт в логи. Workflow — это исполняемый код с доступом к репозиторию, network, artifacts, cache, GITHUB_TOKEN и иногда к production-секретам. Поэтому GitHub Actions надо рассматривать как часть supply chain, а не как «YAML для CI».

Минимальная формула такая: любой run, любой uses и любой context из внешнего события — потенциальная граница доверия. PR title, branch name, issue body, commit message, artifact из другого workflow и код из fork — это untrusted input, пока вы не доказали обратное.

flowchart TD A[Event: push, pull_request, workflow_dispatch] --> B[Workflow YAML] B --> C{Trust boundary} C -->|trusted branch| D[Build/test job] C -->|untrusted PR / issue text / artifact| E[Untrusted input] E --> F[Shell run / third-party action] F --> G{Available authority} G --> H[GITHUB_TOKEN permissions] G --> I[Secrets and env] G --> J[Workspace, cache, artifacts] H --> K[Repo write / issues / packages / releases] I --> L[Cloud deploy / package publish] J --> M[Poisoned artifact or cache] K --> N[Impact] L --> N M --> N
flowchart TD
  A[Event: push, pull_request, workflow_dispatch] --> B[Workflow YAML]
  B --> C{Trust boundary}
  C -->|trusted branch| D[Build/test job]
  C -->|untrusted PR / issue text / artifact| E[Untrusted input]
  E --> F[Shell run / third-party action]
  F --> G{Available authority}
  G --> H[GITHUB_TOKEN permissions]
  G --> I[Secrets and env]
  G --> J[Workspace, cache, artifacts]
  H --> K[Repo write / issues / packages / releases]
  I --> L[Cloud deploy / package publish]
  J --> M[Poisoned artifact or cache]
  K --> N[Impact]
  L --> N
  M --> N
Threat model для GitHub Actions: событие приводит к execution, execution получает authority, authority создаёт side effects.

Основные активы и атакующие

В типичном workflow защищают не только исходники. В зоне риска:

Атакующий может быть внешним contributor из fork, maintainer с ограниченными правами, compromised third-party action, dependency-install script или человек, который получил право редактировать workflow-файлы. Для public repo самый опасный сценарий часто выглядит скучно: человек открывает PR, workflow запускает тесты, а job случайно даёт untrusted code доступ к token или secret.

Least privilege для GITHUB_TOKEN

GITHUB_TOKEN создаётся для workflow автоматически. Важная деталь: action может обратиться к нему через github.token, даже если вы явно не передали ${{ secrets.GITHUB_TOKEN }} в with или env. Поэтому базовая защита — задавать permissions явно и как можно уже.

Плохой default для CI:

name: ci
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v6
      - run: npm ci
      - run: npm test

Такой workflow может работать, но security-review не видит намерения. Лучше записать контракт:

name: ci
on: [push, pull_request]

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v6
      - run: npm ci
      - run: npm test

А job, которому действительно нужно писать issue, получает точечное право:

jobs:
  create-issue:
    runs-on: ubuntu-24.04
    permissions:
      contents: read
      issues: write
    steps:
      - run: gh issue create --title "CI failed" --body "See run logs"
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Это продолжение темы Jobs, dependencies и conditions: права должны соответствовать конкретной job, а не всему workflow «на всякий случай».

Script injection: contexts внутри shell

Самая неприятная категория багов — когда выражение из Contexts и expressions подставляется прямо в shell script. Например, PR title контролирует автор PR:

- name: Unsafe title check
  run: |
    if [[ "${{ github.event.pull_request.title }}" =~ ^release ]]; then
      echo "release PR"
    fi

Если title содержит кавычки, command substitution или перевод строки, вы уже строите shell script из untrusted input. Безопаснее передать значение через env и обращаться к shell variable в кавычках:

- name: Safe title check
  env:
    TITLE: ${{ github.event.pull_request.title }}
  run: |
    if [[ "$TITLE" =~ ^release ]]; then
      echo "release PR"
    fi

Ещё лучше, если логика сложная, вынести её в JavaScript actions или Composite actions, где вход обрабатывается как аргумент, а не как кусок генерируемого shell. Для сообщений в логах и job summary помните про Workflow commands и summaries: не печатайте сырой untrusted текст как command.

ИнтерактивДоверенное или опасное?#int-1
Отнесите элементы к правильной категории threat model.
Untrusted input
Credential/authority
Supply chain
Safer mitigation
Разложите элементы workflow по зонам риска. Это помогает быстро увидеть, где нужна защита.

Secrets и логи

Secrets — не магический сейф. GitHub маскирует известные secret values в логах, но автоматическая redaction не гарантирована для всех трансформаций. Особенно плохая идея — хранить большой JSON/YAML blob как один secret: если workflow распечатает отдельное поле, точного совпадения может не быть.

Практичный baseline:

- name: Deploy
  env:
    API_TOKEN: ${{ secrets.PROD_API_TOKEN }}
  run: |
    ./deploy --token "$API_TOKEN"

И что не надо делать:

- run: |
    echo "Deploying with token ${{ secrets.PROD_API_TOKEN }}"
    curl "https://example.com/deploy?token=${{ secrets.PROD_API_TOKEN }}"

Если workflow генерирует новый чувствительный value из secret, например signed JWT, его тоже надо маскировать через ::add-mask::.... Если secret всё-таки попал в лог без маски, считайте его скомпрометированным: удалите лог, rotate secret, проверьте downstream-доступы.

Для production-deploy лучше использовать environment secrets и protection rules из Environments и protection rules: job не получает environment secrets до approval. Для облаков современный путь — short-lived credentials через OIDC и секреты облаков, а не долгоживущий cloud key в GitHub secret.

Third-party actions и pinning

Каждая строка uses: owner/action@ref — импорт чужого кода в ваш runner. Если action скомпрометирована, она может читать workspace, env, доступные secrets и token permissions этой job. Поэтому Marketplace actions и pinning — security-тема, а не эстетика YAML.

Для high-trust internal repo может быть приемлемо:

- uses: actions/setup-node@v6

Для строгой supply-chain политики лучше full SHA:

- uses: actions/setup-node@8f3c2a4b9d2e1f0a6c7d8e9f0011223344556677

Tag удобнее обновлять, но обычный tag можно передвинуть или удалить. Full SHA остаётся самым прямым immutable reference для action-кода; если у action включены GitHub immutable releases, release-specific tag тоже защищён от перемещения и удаления. Компромисс обычно такой: pin по SHA, включить Dependabot updates для GitHub Actions, а обновления принимать через PR с review. Для Reusable workflows логика такая же: caller импортирует не просто YAML, а privileged automation.

Untrusted PR и privileged workflow

pull_request обычно подходит для read-only validation: checkout PR-кода, установка зависимостей, тесты. pull_request_target запускается в контексте base repository и может иметь доступ к secrets/write-token. Он нужен для узких privileged задач — например, поставить label или написать comment от имени репозитория. Но checkout untrusted PR-кода внутри pull_request_target — классический путь к компрометации.

Безопасный дизайн разделяет два workflow:

# read-only validation
on: pull_request
permissions:
  contents: read
# privileged automation without running PR code
on: pull_request_target
permissions:
  contents: read
  pull-requests: write

Подробно этот edge case разбирается в pull_request_target и untrusted PR. Главное правило здесь: privileged workflow не должен выполнять код, скрипты, build steps или artifacts, которыми управляет автор PR.

Мини-чеклист threat model

Перед тем как считать workflow production-ready, пройдитесь по вопросам:

  • Какие jobs выполняют untrusted code?
  • Какие jobs получают secrets или write-token?
  • Есть ли permissions на workflow и job level?
  • Где shell получает данные из github.event, inputs, branch names или issue text?
  • Все ли third-party actions pinned достаточно строго для вашего риска?
  • Может ли artifact/cache из менее доверенного workflow попасть в privileged workflow?
  • Что останется на self-hosted runner после job?
  • Кто review’ит изменения в .github/workflows?

Это и есть рабочая модель угроз: не абстрактная paranoia, а карта «input → execution → credentials → side effect». Дальше её можно усиливать через org policies, required workflows, runner isolation и наблюдаемость в Monitoring и troubleshooting и Enterprise policies и migration.

See also

Внешние справки: GitHub Docs по Secure use, GITHUB_TOKEN, secrets и script injections; GitHub Well-Architected recommendations for Actions security.

Быстрое повторение #rc-1

Почему workflow в GitHub Actions нужно считать частью supply chain, а не просто YAML для CI?

Показать ответ

Workflow выполняет код с доступом к repo, network, artifacts, cache, GITHUB_TOKEN и иногда production secrets. Любая строка run или uses может стать точкой исполнения чужого или скомпрометированного кода.

Источники

  1. GitHub Docs: Secure use reference
  2. GitHub Docs: Use GITHUB_TOKEN for authentication in workflows
  3. GitHub Docs: Secrets reference
  4. GitHub Docs: Script injections
  5. GitHub Well-Architected: Actions security recommendations