Harden GitHub Actions
This guide walks you through reviewing and hardening your GitHub Actions workflows against supply chain attacks — covering SHA pinning, permissions scoping, and automated update configuration.
Never rely on your knowledge of whether an action version is safe. Run the checklist.
1. List your workflow files
ls .github/workflows/
Work through every file in the directory. A finding in one workflow is often present in others.
2. Check every uses: reference for SHA pinning
A mutable tag (@v4) can be silently repointed to a different commit by anyone with push access to that repository. The tj-actions/changed-files incident (March 2025) demonstrated this at scale — a compromised action exfiltrated secrets from thousands of repositories.
Every uses: reference must be pinned to a full 40-character commit SHA.
Find unpinned references:
grep -r "uses:" .github/workflows/ | grep -v "@[0-9a-f]\{40\}"
Any output is a finding.
3. Resolve the SHA for each action
# Get the SHA for a tag — replace owner, repo, and tag as needed
gh api repos/actions/checkout/git/ref/refs/tags/v4 --jq '.object.sha'
# If the tag points to a tag object rather than a commit, dereference it:
gh api repos/actions/checkout/git/tags/<sha-from-above> --jq '.object.sha'
4. Pin every reference and add a comment
# Before (mutable tag):
uses: actions/checkout@v4
# After (pinned SHA with tag as comment):
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
Always leave the tag as a comment. A bare SHA is unreadable to humans.
Apply the same treatment to third-party actions — any namespace outside actions/ and github/ is high risk:
| Action namespace | Risk level |
|---|---|
actions/* | Medium — GitHub-owned; employee accounts can be compromised |
github/* | Medium — same as above |
| Any other owner | High — no privileged relationship; pin and verify provenance |
5. Add a permissions: block to every workflow
Without an explicit permissions: block, a workflow inherits the repository’s default token permissions, which may include write access to contents, pull requests, or packages.
Most CI workflows (read-only):
permissions:
contents: read
Workflows that post PR comments:
permissions:
contents: read
pull-requests: write
Workflows that upload release artifacts:
permissions:
contents: write
Apply the block at the job level when different jobs need different scopes. Apply at the workflow level when all jobs share the same scope.
6. Check for pull_request_target misuse
pull_request_target runs with write permissions and access to secrets. If used with actions/checkout of the PR head, it allows a fork to run arbitrary code with those permissions.
grep -r "pull_request_target" .github/workflows/
Any match warrants careful review. If the trigger is needed, ensure the checkout step checks out the base branch, not the PR head.
7. Check for script injection
User-controlled input flowing into run: shell commands is a script injection risk:
grep -r "\$
run: echo "$PR_TITLE"
8. Configure Dependabot to keep pinned SHAs current
A pinned SHA that never updates is worse than a mutable tag once a CVE lands in the pinned version. Add a dependabot.yml entry for GitHub Actions:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
groups:
actions:
patterns:
- "*"
9. Produce a findings table
## Supply Chain Assessment — .github/workflows/
| File | Finding | Severity | Fix |
| --- | --- | --- | --- |
| lint.yml | Third-party action not SHA-pinned | High | Pin to commit SHA |
| build.yml | No permissions block | Medium | Add permissions: contents: read |
| All files | No dependabot.yml for actions | Medium | Add .github/dependabot.yml |
Severity guide: High — third-party action not SHA-pinned; pull_request_target misuse; script injection. Medium — first-party action not SHA-pinned; no permissions: block; no Dependabot. Low — minor hygiene.
Summary
After completing these steps you have:
- Every
uses:reference pinned to a full commit SHA with a human-readable comment - Third-party actions identified and risk-rated
- Minimal
permissions:blocks on every workflow - No
pull_request_targetmisuse or unguarded script injection - Dependabot configured to keep pinned SHAs current automatically