uses: actions/checkout@v4 — almost every line of your CI is probably written like this. But that @v4 is "mutable." A tag can be reassigned to a different commit anytime. In other words, an action you audited and trusted today might morph into different code tomorrow. This is the core of GitHub Actions' supply-chain risk, and past actual breaches (theft of secrets via rewriting a popular action's tag) exploited exactly this structure.
The countermeasure is clear. Pin the action to a commit SHA (immutability) and keep it safely updated with Dependabot. This article explains that practice as a topic of the Dependabot production-operations guide.
Rules for this article: the recommended patterns are based on GitHub's official security-hardening guide (as of June 2026). Since CI security evolves fast in both attack techniques and countermeasures, always confirm the latest in the official Secure use reference before production application.
0. Why a tag reference is dangerous
GitHub Actions references have three levels, with different immutability.
| Reference | Example | Immutable | Risk |
|---|---|---|---|
| Major tag | @v4 | ✗ mutable | Can point to a different commit anytime. Most dangerous |
| Full-version tag | @v4.2.0 | ✗ mutable | Semantic, but the tag can be reassigned |
| Commit SHA | @a1b2c3... (40 chars) | ✓ immutable | Pinned to audited code. Recommended |
A tag is just a pointer. If an attacker hijacks an action's repository, they can redirect v4 to a backdoored commit. You changed nothing, yet malicious code runs on the next CI run — this is the attack that exploits "tag mutability."
1. SHA pinning = the only immutable reference
The countermeasure is to fix to the full commit SHA.
# Before(可変・危険)
- uses: actions/checkout@v4
# After(不変・推奨)— SHA に固定し、末尾コメントで版を可読に
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
Why it's safe:
- Immutable: the SHA is a hash of the commit contents. The code it points to never changes. It's fixed to the code at the moment you audited and trusted it.
- Tamper-resistant: making different code appear as the same SHA requires producing a SHA-1 collision with a valid Git object, which is practically hard.
- Readable: the trailing
# v5.0.0comment lets humans see the version at a glance. And this comment is rewritten by Dependabot on update (next chapter).
Use the 40-char full SHA, not a short SHA or a tag reference. This is the only way to use an action as an "immutable release."
2. Dependabot can update SHA pins
"If I fix it to a SHA, won't it stop updating?" — a common misconception. Dependabot understands SHA pinning.
When a new version comes out, Dependabot opens a PR that updates the SHA and the trailing version comment together. It's the exact same update experience as with a tag reference, and you can always stay with an immutable reference.
# Dependabot が開くPRの差分イメージ
- - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
+ - uses: actions/checkout@<新しいSHA> # v5.1.0
The configuration is just adding package-ecosystem: github-actions.
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/" # .github/workflows/ を見る
schedule:
interval: "weekly"
commit-message:
prefix: "ci"
groups:
actions:
patterns: ["*"] # アクション更新を1PRに束ねる
"Immutability (SHA pin)" and "freshness (Dependabot)" aren't a trade-off; they can coexist — this is the most important point of this article. Take pinning's safety while updates catch up automatically.
3. Bulk-pin an existing repository
Hand-SHAing dozens of workflows isn't realistic. Use an auto-pinning tool.
pinact(OSS CLI): resolveuses:tags and bulk-convert to full SHA + version comment.- Services like StepSecurity's Secure Workflows / Harden-Runner also provide pinning and auditing.
# 例:pinact でリポジトリ内のアクションを一括ピン留め
pinact run
After introduction, Dependabot maintains and updates the commented SHAs, so once pinned, operations keep running. Adding a CI check that "fails if it detects an unpinned action" also prevents regression.
4. Defense in depth: don't rely on SHA pins alone
SHA pinning is powerful, but alone it's not a silver bullet. Layer it.
4.1 Least-privilege GITHUB_TOKEN
State and narrow the workflow's permissions. Don't leave the default broad permissions.
permissions:
contents: read # 既定を絞り、必要なジョブだけ昇格する
Even if an action is compromised, if what the token can do is small, the damage is limited. This is the same principle as in Dependabot's auto-merge workflow.
4.2 Allow only trusted actions
Restrict usable actions with the Actions policy at the organization/repository level. GitHub also provides a policy that requires SHA pinning (forcing allowed actions to be SHA references). Just narrowing the "anyone can uses: any action they like" state greatly reduces the attack surface.
4.3 Audit third parties
- Official (
actions/*,github/*) is relatively low-risk. - For third-party actions, audit the source before pinning, and check the star count, maintenance status, and permission requests.
- Even after pinning, monitor for maintainer changes (below) in Dependabot PRs.
5. Auto-merge actions "carefully"
You can auto-merge action patch/minor too, but be one notch more careful from a supply-chain standpoint. The reason is that an action update is a change to the executing code of CI itself.
Use dependabot/fetch-metadata's maintainer-changes to route updates where the maintainer changed to human review. A maintainer change can be a sign of a hijack.
- name: Hold actions PRs with maintainer changes
if: |
steps.meta.outputs.package-ecosystem == 'github_actions' &&
steps.meta.outputs.maintainer-changes == 'true'
run: |
gh pr comment "$PR_URL" --body "⚠️ アクションのメンテナ変更を検知。供給網観点で人間レビュー必須。"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Auto-merge trusted first-party action patches
if: |
steps.meta.outputs.package-ecosystem == 'github_actions' &&
steps.meta.outputs.maintainer-changes != 'true' &&
steps.meta.outputs.update-type == 'version-update:semver-patch'
run: gh pr merge --auto --squash "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Policy: lean toward auto for official-action patches, humans for third-party and minor/major and maintainer changes. Automation proportional to risk.
6. Checklist
- Pin all
uses:to a full 40-char SHA + version comment (bulk withpinact, etc.) - Add
package-ecosystem: github-actionstodependabot.ymland bundle withgroups - State the workflow's
permissionsat least privilege - Restrict allowed actions with an Actions policy (enforce SHA pinning if possible)
- Audit third-party before introduction, monitor maintainer-changes after
- Limit action auto-merge to official patches, human review for the rest
- Add a guard that "fails CI if it detects an unpinned action"
7. FAQ
Q. Doesn't SHA pinning stop updates? A. It doesn't. Dependabot understands SHA pinning and, when a new version comes out, opens a PR updating the SHA and version comment together. Immutability and freshness coexist.
Q. Isn't it enough to use Dependabot with @v4 as-is?
A. You can follow updates, but the tag-mutability risk remains. A tag can be reassigned anytime, and if an attacker hijacks it, they can point v4 at a malicious commit. Only the SHA is immutable.
Q. Do official actions (actions/checkout, etc.) also need SHA fixing? A. The risk is relatively low, but pinning all actions for consistency is recommended. Since Dependabot maintains the updates, the operational load doesn't increase.
Q. How do I convert a large number of existing workflows?
A. You can bulk-pin with an auto tool like pinact. It resolves tags into full SHA + comment, and Dependabot maintains them thereafter.
Q. May I auto-merge actions too?
A. Lean toward auto for official-action patches, and route maintainer changes (maintainer-changes), third-party, and minor/major to human review. Since an action update is a change to CI's executing code, be one notch more careful.