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Основные активы и атакующие
В типичном workflow защищают не только исходники. В зоне риска:
- repository write-доступ через Permissions и GITHUB_TOKEN;
- cloud credentials, npm/PyPI/Docker tokens и environment secrets из Variables, env и secrets;
- release artifacts, container images и package publish из Release automation и Packages и container images;
- cache, artifacts и reports из Artifacts и reports и Dependency cache;
- self-hosted runner как машина, сеть и persistent filesystem; это отдельная тема Runner governance.
Атакующий может быть внешним 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.
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@8f3c2a4b9d2e1f0a6c7d8e9f0011223344556677Tag удобнее обновлять, но обычный 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
- Permissions и GITHUB_TOKEN — минимальные права для workflow и отдельных jobs.
- Variables, env и secrets — где хранить конфигурацию и чувствительные значения.
- Marketplace actions и pinning — supply-chain trust для
uses:. - pull_request_target и untrusted PR — безопасные паттерны для fork PR и privileged automation.
- OIDC и секреты облаков — short-lived cloud credentials вместо долгоживущих secrets.
- Runner governance — isolation, labels, groups и self-hosted runner policy.
- Monitoring и troubleshooting — логи, reruns, debug logging и расследование flaky/security failures.
Внешние справки: GitHub Docs по Secure use, GITHUB_TOKEN, secrets и script injections; GitHub Well-Architected recommendations for Actions security.
