GitHub Actions Security Checklist: 12 Things to Audit Before You Ship
GitHub Actions is powerful — and that power cuts both ways. A misconfigured workflow can leak secrets, allow unauthorized code execution, or let attackers pivot into your production environment.
Here's the checklist I run through before shipping any workflow.
1. Pin third-party actions to a full commit SHA
# ❌ Dangerous — tag can be moved
- uses: actions/checkout@v4
# ✅ Safe — immutable
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
Tags are mutable. An attacker who compromises an upstream action repo can push a new commit to the v4 tag. Pinning to a SHA means you get exactly what you audited.
Tool that catches this: workflow-guardian flags unpinned actions automatically.
2. Never use pull_request_target without extreme caution
pull_request_target runs with write permissions and access to secrets — even for PRs from forks. Combined with actions/checkout on the PR head, you have a critical vulnerability:
# ❌ Pwn request pattern — DO NOT DO THIS
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 install && npm test # attacker controls this code
If you need pull_request_target, never check out or execute code from the PR branch.
3. Limit GITHUB_TOKEN permissions
Default token permissions vary by org settings. Be explicit:
permissions:
contents: read # only what you need
pull-requests: write # if you post PR comments
Or lock everything down at the workflow level and grant up per-job:
permissions: read-all # workflow default
jobs:
deploy:
permissions:
contents: write # only this job gets write
4. Don't echo untrusted input into $GITHUB_ENV or $GITHUB_OUTPUT
This is an environment variable injection vector:
# ❌ Dangerous if PR title contains shell metacharacters
- run: echo "BRANCH=${{ github.event.pull_request.title }}" >> $GITHUB_ENV
# ✅ Use an intermediate env var
- run: echo "BRANCH=$TITLE" >> $GITHUB_ENV
env:
TITLE: ${{ github.event.pull_request.title }}
5. Validate workflow syntax before merge
Broken workflows fail silently or at the worst moment. Lint them:
- name: Lint workflows
uses: ollieb89/workflow-guardian@v1
This catches syntax errors, deprecated features, and security anti-patterns before they hit main.
6. Don't store secrets in workflow files
Obvious, but it happens:
# ❌ Never do this
env:
API_KEY: sk-prod-abc123xyz
# ✅ Use GitHub Secrets
env:
API_KEY: ${{ secrets.API_KEY }}
Audit your repo history too — git log -S 'sk-' --all can surface old leaks.
7. Restrict who can trigger workflow_dispatch
Anyone with write access can trigger manual workflows by default. If your dispatch workflow deploys to production, add an environment with required reviewers.
8. Use environments for production deployments
jobs:
deploy:
environment: production # requires approval from configured reviewers
Environments give you deployment protection rules, required reviewers, and scoped secrets.
9. Check for script injection in run steps
Any expression interpolated directly into a run block is a script injection risk:
# ❌ Vulnerable to script injection
- run: echo "PR author is ${{ github.event.pull_request.user.login }}"
# ✅ Use env vars
- run: echo "PR author is $AUTHOR"
env:
AUTHOR: ${{ github.event.pull_request.user.login }}
10. Don't use self-hosted runners for public repos
Anyone can fork a public repo and submit a PR that runs on your self-hosted runner. Unless you have strict protections in place, use GitHub-hosted runners for public repos.
11. Rotate secrets regularly and audit access
- Check Settings > Secrets for stale/unused entries
- Rotate any secret that may have been exposed in logs
- Grep your workflow run logs for accidental secret prints:
***masking doesn't catch every format
12. Enable Dependabot for action version updates
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
This keeps your pinned actions updated with security patches automatically.
Automate as Much as Possible
This checklist is a lot to remember. Most of it can be caught automatically:
- workflow-guardian — lints your workflow files on every PR, catching security issues and syntax errors before they merge
- actionlint — static analysis for workflow syntax
- Dependabot — keeps action versions current
The goal isn't to memorize all of this — it's to build the guardrails so you don't have to.
Found this useful? I build open-source tools for GitHub Actions security. Check out workflow-guardian — it automates most of this checklist.
Top comments (0)