GitHub Actions security checklist
Summary
A GitHub Actions security review should not stop at YAML syntax. Review triggers, GITHUB_TOKEN permissions, secrets, third-party actions, runners, deployment identity, and untrusted input as separate security boundaries.
Example: Least-Privilege CI Workflow
This is a practice CI workflow. It runs tests on pull requests and grants the GITHUB_TOKEN only repository contents read access. Replace <full-length-sha> with the reviewed action commit SHA.
name: ci
on:
pull_request:
branches: [main]
permissions: {}
jobs:
test:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@<full-length-sha>
- name: Run tests
run: npm test
Example successful output:
Run npm test
...
✓ all tests passed
This is example output. Real output depends on the project’s test runner.
Bad and Fixed Examples
Bad example:
permissions: write-all
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: echo "${{ github.event.pull_request.title }}" | bash
Problems:
write-allgrants write permissions that a test job does not need.actions/checkout@v4pins only a tag, so tag movement and supply-chain risk need separate review.- A pull request title is attacker-controlled input and should not flow directly into shell code.
Fixed example:
permissions: {}
jobs:
test:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@<full-length-sha>
- name: Print PR title as data
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
printf '%s\n' "$PR_TITLE"
Replace <full-length-sha> with the reviewed action commit SHA. The PR title is passed as data through an environment variable instead of being inserted directly into shell source.
Failure Log Example
After reducing token permissions, an existing workflow that tries to write to the repository can fail like this:
remote: Permission to <owner>/<repo>.git denied to github-actions[bot].
fatal: unable to access 'https://github.com/<owner>/<repo>/': The requested URL returned error: 403
Do not immediately broaden permissions. First ask:
- Does this job really need write access?
- If yes, is the required permission
contents: write,packages: write, orpull-requests: write? - Can the write operation move from a pull request job to a release or deploy job?
Document Information
- Written date: 2026-04-29
- Verification date: 2026-06-05
-
Document type: analysis tutorial - Test environment: No live execution. This checklist is based on official GitHub Actions security documentation.
- Test version: GitHub Docs checked on 2026-06-05. No specific runner image or workflow execution version is fixed.
- Evidence level: official documentation
Problem Statement
GitHub Actions can handle repository code, secrets, releases, packages, and cloud deployment credentials in one workflow. That makes even a small workflow part of the software supply chain. PR-controlled strings, third-party actions, broad GITHUB_TOKEN permissions, self-hosted runners, and long-lived cloud credentials should be reviewed explicitly.
This post gives a minimum checklist for adding or reviewing a workflow.
Verified Facts
- GitHub documentation explains that attacker-controlled contexts can be interpreted as executable code in workflows, custom actions, and composite actions. Evidence: GitHub Actions script injections
- An action can access
GITHUB_TOKENthrough thegithub.tokencontext even if the workflow does not explicitly pass it, so GitHub recommends limiting token permissions. Evidence: Use GITHUB_TOKEN for authentication in workflows - The
permissionskey is the official way to modifyGITHUB_TOKENpermissions at workflow or job scope. Evidence: Modifying the permissions for the GITHUB_TOKEN - GitHub’s secure use reference recommends pinning third-party actions to a full-length commit SHA. Evidence: GitHub Actions secure use reference
- GitHub documentation recommends OIDC for cloud-provider authentication to reduce long-lived cloud secrets. Evidence: OpenID Connect in GitHub Actions
Reproduction Steps
Review a workflow in this order.
- Check triggers.
- Compare
pull_request,pull_request_target,workflow_run,workflow_dispatch, andschedule. - Review whether forked PRs need secrets or write permissions.
- Do not place PR titles, branch names, issue bodies, or commit messages directly into shell code.
- Reduce
GITHUB_TOKENpermissions per job.
permissions: {}
jobs:
test:
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<full-length-sha>
- Review third-party actions.
- Prefer full-length commit SHA pinning over
uses: owner/repo@v1. - Check source code, provenance, maintainers, update process, and Dependabot alerts.
- Apply the same review to composite actions and reusable workflows.
- Scope secrets and environments.
- Do not provide deployment secrets to test jobs.
- Use production environments with required reviewers and branch/tag restrictions.
- Do not print secrets or pass them as command-line arguments.
- Review runners.
- Self-hosted runners are especially risky for public repositories.
- Check whether the runner can reach internal networks, cloud metadata, Docker socket, or deployment credentials.
- Review whether workspaces, caches, or artifacts can move secrets between jobs.
- Review deployment identity.
- Prefer OIDC over long-lived cloud keys where possible.
- Restrict OIDC subject, audience, repository, branch, and environment in the cloud IAM policy.
Observations
- Without explicit
permissions, a workflow depends on repository defaults, which makes review harder. - A compromised third-party action can access secrets and token permissions available to its job.
- Script injection is not just a YAML issue. It happens when untrusted context flows into shell, JavaScript, or API calls.
Interpretation
In my view, the core question is “which code runs with which authority?” That means triggers, token permissions, action provenance, secret scope, and runner isolation must be reviewed together.
Opinion: start every workflow with permissions: {} or a read-only default, then add write permissions only to the job that needs them.
Limitations
- GitHub Enterprise settings, organization policies, repository visibility, and fork settings can change details.
- This checklist is GitHub Actions-specific. It does not directly cover GitLab CI, Jenkins, or CircleCI.
- Incident response also needs audit logs, secret rotation, runner forensics, and dependency review output.
- The workflow and logs above are examples. Before publication, recheck GitHub Actions workflow syntax,
GITHUB_TOKENpermission names, runner images, and action pinning guidance.
Pre-publication Recheck Required
This is a scheduled post. Before publishing, recheck the items below.
GITHUB_TOKENpermission names and default permission policy have not changed.- Workflow syntax for
permissions,pull_request,pull_request_target, andworkflow_runstill matches this post. - Third-party action pinning guidance in the GitHub Actions secure use reference has not changed.
- Official secrets and OIDC documentation URLs still resolve.
- The
ubuntu-latestrunner image and default toolchain assumptions are still valid.
References
- GitHub Actions secure use reference
- GitHub Actions script injections
- Use GITHUB_TOKEN for authentication in workflows
- GITHUB_TOKEN
- GitHub Actions secrets reference
- OpenID Connect in GitHub Actions
Change Log
- 2026-04-29: Initial draft.
- 2026-04-29: Added trigger, token, action pinning, secret, runner, and OIDC checks based on GitHub Actions official documentation.
댓글남기기