はじめに
GitHub Actions のセキュリティ対策として「SHA pinning(コミット SHA 固定)」がよく推奨されます。一方で、npm や Go、Terraform などのパッケージ管理では、開発者がハッシュを手書きする場面はほとんどありません。
- SHA pinning とは何か、なぜサプライチェーン攻撃の対策になるのか
- GitLab CI/CD でも同様の対策ができるのか
- なぜ言語パッケージの世界では SHA を手書きしないのか(実は裏で同等のことが行われている)
本記事では、この 3 点を整理します。
GitHub Actions の SHA pinning とは
GitHub Actions では、ワークフローで使うアクションを次の 2 通りの方法で参照できます。
# タグ参照(可変)
- uses: actions/checkout@v4
# コミット SHA 参照(不変)
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
タグ参照の何が危険なのか
Git のタグは、後から別のコミットに付け替えることができます。
アクションのリポジトリやメンテナのアカウントが侵害された場合、攻撃者は既存のタグ(v4 など)を悪意あるコミットに移動できます。すると、そのアクションを利用している全リポジトリのワークフローで、次回実行時から悪性コードが動いてしまいます。
これは机上の話ではなく、2025 年 3 月の tj-actions/changed-files 事件で実際に発生しました。タグが書き換えられ、このアクションを使っていた多数のリポジトリで CI のシークレットが漏洩しています。
SHA pinning が対策になる理由
コミット SHA は Git のオブジェクトハッシュそのものであり、同じ SHA が別の内容を指すことはありません。SHA で参照しておけば、タグの付け替えによる攻撃経路を塞げます。
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
コメントでバージョンを併記しておくと、可読性も保てます。
SHA pinning の限界と併用すべき対策
SHA pinning が保証するのは「参照先が変わらないこと」だけです。以下の点には注意が必要です。
- pin した時点のコードが安全かどうかは別問題(pin する前に内容を確認する)
- アクションが内部で呼び出す別のアクション(推移的依存)までは固定できない
- 更新が手動になるため放置されやすい → Dependabot / Renovate は pinned SHA を認識して更新 PR を出せるので併用する
-
GITHUB_TOKENの権限最小化、egress 制限(Harden-Runner 等)と組み合わせる
GitLab CI/CD でも同様の対策はできるのか
GitLab には GitHub Actions の「アクション」に完全対応する概念はありませんが、「可変な参照(タグ・ブランチ)を不変な参照(SHA / digest)に置き換える」という同じ原則で対策できます。
1. include の ref をコミット SHA で固定する
外部プロジェクトの CI テンプレートを取り込む際、ref にコミット SHA を指定できます。
include:
- project: 'group/ci-templates'
ref: 8f1e6b2c1234567890abcdef1234567890abcdef # コミット SHA で固定
file: '/templates/build.yml'
2. CI/CD Components も SHA 参照できる
GitHub Actions に近いコンポーネント機構である CI/CD Components も、セマンティックバージョンだけでなくコミット SHA で参照でき、SHA 指定が最も強い固定になります。
3. コンテナイメージは digest で固定する
GitLab CI はジョブを Docker イメージ上で実行することが多いため、イメージの固定も重要です。
# タグ参照(可変)
image: node:20
# digest 参照(不変)
image: node@sha256:1234abcd...
これは GitHub Actions でコンテナを使う場合も同様です。
言語パッケージや Terraform では SHA pinning していないのか?
npm、Go、Java、Terraform などでは、開発者がハッシュを手書きする場面はほぼありません。しかし「SHA pinning をしていない」わけではなく、多くのエコシステムはロックファイルによって裏でハッシュ固定・検証を行っています。
鍵は「二層構造」
開発者が書くマニフェストと、ツールが自動生成するロックファイルの二層に分かれています。
package.json : "express": "^4.18.0" ← 人間が書く「意図」(可変)
package-lock.json : express 4.18.2
integrity: sha512-xxxx… ← ツールが自動生成する「固定」(不変)
npm install した瞬間に、解決された正確なバージョンと成果物のハッシュがロックファイルへ自動記録されます。以後 npm ci はこのハッシュと一致しないパッケージを拒否します。
つまり開発者は「バージョン指定」という読みやすい操作だけを行い、ハッシュ固定はツールが透過的に実施するという分業になっています。この二層構造により、
- マニフェスト側:可読性・更新のしやすさ
- ロックファイル側:再現性・改ざん検知
を両立できます。
エコシステムごとの成熟度
| エコシステム | 仕組み | 備考 |
|---|---|---|
| JavaScript (npm/yarn/pnpm) |
package-lock.json / yarn.lock の integrity (SHA-512) |
npm ci で検証。ロックファイルのコミットが前提 |
| Go |
go.sum + checksum database (sum.golang.org) |
最も厳格な部類。透明性ログで全世界的に同一内容を保証 |
| Terraform |
.terraform.lock.hcl のハッシュを terraform init で検証 |
module の Git 参照は ?ref=<コミットSHA> で別途固定が必要 |
| Python (pip) |
--require-hashes や Poetry / uv のロックファイル |
デフォルトでは検証しない。オプトイン |
| Java (Maven/Gradle) | Gradle の dependency verification (verification-metadata.xml) |
標準では無効。運用負荷が高く普及率は低い。相対的に弱い領域 |
防御が効いていない運用パターン
仕組みがあっても、次のような運用ではハッシュ検証が機能しません。
- ロックファイルをコミットしていない
- CI で
npm ciではなくnpm installを使っている - Gradle で dependency verification を有効にしていない
- Terraform の module をタグ参照のままにしている
なぜ GitHub Actions だけ明示的に SHA を書くのか
ここまでの内容を踏まえると、答えはシンプルです。GitHub Actions にはロックファイルに相当する層が存在しないからです。
npm : ^4.18.0 (可変) → ロックファイルが固定 → 実行は不変
Actions : @v4 (可変) → 固定する層がない → 実行のたびに参照先が変わりうる
uses: actions/checkout@v4 と書くと、実行のたびに GitHub が「いま v4 タグが指しているコミット」を解決しに行きます。解決結果を記録して次回も同じものを使う仕組みがないため、開発者が唯一の不変な参照であるコミット SHA を手で YAML に書き込むしかありません。
言語エコシステムでハッシュを手書きしないのは「書かなくてもツールが裏でやってくれるから」、Actions で手書きするのは「やってくれる仕組みがないから」です。
なぜ Actions にロックファイルがないのか
設計上の経緯によるものです。ワークフロー YAML は「宣言的な設定」として設計され、依存解決フェーズを持たず、参照を実行時にそのまま解決する素朴なモデルで始まりました。
また、タグ参照(@v4)の推奨は、アクション作者がメジャータグを最新パッチに動かすことで後方互換なバグ修正を配りやすくするためでした。この「タグが動くことが前提」の設計が、そのままセキュリティ上の弱点になっています。
まとめ
- GitHub Actions の SHA pinning は、タグ付け替えによるサプライチェーン攻撃への対策として有効
- GitLab でも
includeの ref・CI/CD Components・コンテナイメージの digest 指定により、同じ原則(可変参照→不変参照) で対策できる - 言語パッケージや Terraform は「SHA pinning していない」のではなく、ロックファイルが裏でハッシュ固定・検証している(二層構造)
- GitHub Actions で SHA を手書きするのは、ロックファイルに相当する層が存在しないため
- どの仕組みも万能ではないため、ロックファイルのコミット、Dependabot / Renovate による更新、権限最小化などの運用と組み合わせることが重要