# Pin GitHub Actions to a SHA and update with Dependabot: a practice to prevent supply-chain attacks

> A practical guide to pinning GitHub Actions' `uses:` from a mutable tag to a commit SHA and keeping it safely updated with Dependabot. Faithful to the official security-hardening guidance (as of June 2026), it explains, with real code, why tag references are dangerous, SHA pinning + version comments, Dependabot's behavior of updating the SHA and comment together, bulk-pinning an existing repository, and defense in depth — least-privilege GITHUB_TOKEN, allowing only trusted actions, and a policy enforcing SHA pinning.

- Published: 2026-06-28
- Author: 友田 陽大
- Tags: Dependabot, GitHub Actions, サプライチェーンセキュリティ, DevSecOps, CI/CD, セキュリティ
- URL: https://tomodahinata.com/en/blog/dependabot-github-actions-sha-pinning-supply-chain-security-guide
- Category: Dependabot & dependency automation
- Pillar guide: https://tomodahinata.com/en/blog/dependabot-production-guide

## Key points

- A tag reference like uses: actions/checkout@v4 is 'mutable.' Since tags can be reassigned, there's a supply-chain risk that an action you trusted is tampered with later. The tj-actions-family incident exploited this structure.
- The only immutable reference is the full commit SHA. Write uses: actions/checkout@<40-char SHA> # v4.2.0, with a trailing comment so humans can read the version. Since a SHA collision is practically hard, you can pin audited code.
- Dependabot understands SHA pinning and, when a new version comes out, opens a PR that updates the SHA and the version comment together. So pinning doesn't stop updates — just set package-ecosystem: github-actions.
- Defense in depth: SHA pinning + Dependabot + least-privilege GITHUB_TOKEN (state permissions) + allowing only trusted actions (enforce SHA pinning with an Actions policy). Don't rely on one; layer them.
- Be careful with auto-merging actions. Look at fetch-metadata's maintainer-changes and route maintainer changes to human review. The more third-party, the more careful; lean toward auto for official (actions/*) patches.

---

`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](/blog/dependabot-production-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](https://docs.github.com/en/actions/reference/security/secure-use) 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.**

```yaml
# 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.0` comment 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.**

```yaml
# 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`](/blog/dependabot-yml-configuration-complete-guide#1-package-ecosystem対応エコシステム).

```yaml
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): resolve `uses:` tags and bulk-convert to **full SHA + version comment.**
- Services like **StepSecurity's Secure Workflows / Harden-Runner** also provide pinning and auditing.

```bash
# 例：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.

```yaml
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](/blog/dependabot-auto-merge-github-actions-automation-guide#3-tokenモデルここがセキュリティの肝).

### 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.**

```yaml
      - 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 with `pinact`, etc.)
- [ ] Add `package-ecosystem: github-actions` to `dependabot.yml` and bundle with `groups`
- [ ] State the workflow's `permissions` at **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.
