テーマを切り替える

GitHub Actions セキュリティ実践:tj-actions 事件から学ぶ 3 つの重要な防御策

2025 年 3 月、GitHub Advisory が私の認識の限界を更新しました。3 年間使い続けてきた tj-actions/changed-files が CVE-2025-30066 としてマークされ、23,000 以上のリポジトリが一晩で Secrets 漏洩リスクに晒されました。

さらに背筋が凍るのが攻撃手法です。攻撃者はまずあるプロジェクトの PAT を窃取し、tj-actions のバージョンタグを改ざんして、もともと安全だったコードを悪意あるスクリプトに差し替えました。AWS キー、DB パスワード、API Token——それらの Secrets が、公開ログへ静かに流れ込んだのです。

当時はかなり参りました。自分で設定した CI/CD フローが、攻撃者の踏み台になっていた。すぐにすべてのリポジトリを洗い、Action バージョン参照、GITHUB_TOKEN 権限、監査ログ設定を確認。丸一日かけて、ずっと見落としていた細部がこんなに多いことに気づきました。

この記事は、その「ヒヤッとした瞬間」のあとに整理した防御チェックリストです。サプライチェーン攻撃の手口、Secrets 管理の正しいやり方、GITHUB_TOKEN 権限制御の細部、そして GitHub 2026 セキュリティロードマップで先に備えておきたい新機能について話します。

ポイントは、これらはすべて今すぐ設定できるもので、新機能のリリースを待つ必要がないことです。

tj-actions 事件から見る CI/CD サプライチェーン攻撃

攻撃はどう起きたか

まず tj-actions/changed-files について。この Action は GitHub で非常に人気があり、PR でどのファイルが変更されたかを検出するために多くの CI/CD フローで使われています。私のプロジェクトも、増分デプロイの判断に依存していました。

攻撃チェーンはおおよそこうです。

攻撃者は reviewdog/action-setup プロジェクトから PAT(個人アクセストークン)を窃取しました。この PAT にはリポジトリへの書き込み権限がありました。トークンを手に入れた攻撃者は、tj-actions リポジトリのバージョンタグを改ざん——もともと安全なコードを指していた v45 タグを、悪意あるロジックを埋め込んだ新しいコミットへ静かに向け替えました。

uses: tj-actions/changed-files@v45 を使い続けていたプロジェクトは、気づかないうちに悪意あるコードを取得しました。悪意あるスクリプトがやったことはシンプルで、CI/CD 環境の Secrets をすべてログに出力しただけです。ログは公開され、Secrets はそのまま漏洩しました。

GitHub Advisory によると、23,000 以上のリポジトリが影響を受けました。Coinbase のセキュリティチームは後に、この攻撃が 70,000 以上の顧客データに波及したと公表しました。

私が踏んだ落とし穴:タグ参照 vs SHA 固定

自分のリポジトリを確認したとき、タグ参照を使っている箇所が多数ありました。

# 誤った例:可変タグを使用
- name: Check changed files
  uses: tj-actions/changed-files@v45

タグは「生きている」存在です。リポジトリメンテナー(または書き込み権限を得た攻撃者)がいつでもタグを新しいコミットに向け直せます。v45 を参照しているつもりが、実際に動いているのは改ざんされた悪意あるコードかもしれません。

正しい方法は完全な SHA で固定することです。

# 正しい方法:完全な SHA を使用
- name: Check changed files
  uses: tj-actions/changed-files@6cbf527e7a7b6d61c4e7f25e5ce5f7b7c8f3c72a

SHA は「固定」です。参照を更新しない限り、実行されるのは常に同じコードで、決して変わりません。

修正しながら自分を責めました。これは基本のセキュリティ常識なのに、なぜずっと気づかなかったのか、と。

サードパーティ Action の信頼コスト

tj-actions 事件はもう一つの問題も露わにしました。サードパーティ Action への信頼が安すぎる、ということです。

stars が多い、使っている人が多い——そんな理由だけで、本番 CI/CD フローにそのまま突っ込んでしまう。でもメンテナーのセキュリティ意識はどうなのか。PAT が窃取されない保証はあるのか。

GitHub 2026 セキュリティロードマップには、ワークフロー級依存ロックという案が挙がっています。package-lock.json のように、すべての Action の SHA をロックファイルに固定する機能です。まだリリースされていませんが、今から手動で——各 Action 参照を SHA に変更し、定期的に監査——できます。

もう一つの提案は、サードパーティ Action の数を減らすこと。公式 Action で済むなら、サードパーティは使わない。ファイル検出なら GitHub 公式の actions/checkout と shell スクリプトで実現でき、tj-actions に依存する必要はありません。

Secrets 管理の 3 層と応用実践

GitHub Secrets の 3 段階ストレージ

GitHub 組み込み Secrets は 3 層に分かれます。組織レベル、リポジトリレベル、環境レベルです。

組織レベル Secrets は複数リポジトリで共有可能で、AWS キーやクラウドサービス Token などの共通資格情報に向いています。リポジトリレベル Secrets は現在のリポジトリでのみ使え、プロジェクト専用の DB パスワードなどに適しています。環境レベル Secrets はより細かく、環境保護ルールと組み合わせられます——例えば PR 承認後にのみ本番環境 Secrets にアクセスできる、といった設定が可能です。

ストレージ面では、GitHub は libsodium sealed box で暗号化します。Secrets は書き込み後に再読み取りできず、ワークフロー実行時のみ secrets コンテキスト経由でアクセスできます。

steps:
  - name: Deploy to AWS
    env:
      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

よくある間違いは、Secrets を shell コマンドに直接展開することです。

# 危険:Secrets がログに出力される可能性がある
- name: Configure AWS
  run: aws configure set aws_access_key_id ${{ secrets.AWS_ACCESS_KEY_ID }}

Secrets の値に特殊文字が含まれていたり、コマンド実行が失敗したりすると、ログに Secrets が露出する可能性があります。正しい方法は環境変数経由で渡すことです。

# 安全:環境変数経由で渡す
- name: Configure AWS
  env:
    AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
  run: aws configure set aws_access_key_id "$AWS_ACCESS_KEY_ID"

動的マスキング:::add-mask::

事前に保存した Secrets 以外に、実行時に生成される機密データもあります。例えばスクリプトが出力する一時 Token です。この場合は ::add-mask:: で動的にマスクできます。

- name: Generate temporary token
  run: |
    token=$(generate-token.sh)
    echo "::add-mask::$token"
    echo "TOKEN=$token" >> $GITHUB_ENV

マスク後の値はログ上 *** と表示されます。ただし注意:マスクは値が出力される前に行う必要があります。先に出力してからマスクしても、ログには露出したままです。

応用:HashiCorp Vault OIDC 連携

セキュリティ要件が高いプロジェクトでは、GitHub 組み込み Secrets だけでは不十分かもしれません。長期資格情報が GitHub に存在し、リポジトリが侵害されれば Secrets もすべて失われます。

より良い方法は HashiCorp Vault を OIDC(OpenID Connect)と組み合わせ、資格情報なしアクセスを実現することです。GitHub Actions が Vault に「自分は誰か」を証明し、Vault が検証後に短期 Token を発行します。GitHub 上に長期資格情報を一切置かなくて済みます。

設定手順の概要は次のとおりです。

ステップ 1:Vault で OIDC ロールを設定

Vault は GitHub の OIDC プロバイダーを信頼する必要があります。どのリポジトリがどの Secrets を取得できるかを指定するロールを設定します。

resource "vault_jwt_auth_backend_role" "github_actions" {
  backend        = "jwt"
  role_name      = "github-actions-role"
  bound_audiences = ["https://github.com/your-org"]
  user_claim     = "repository"
  role_type      = "jwt"
  
  token_policies = ["ci-policy"]
  token_ttl      = "1h"
}

ステップ 2:GitHub Actions で vault-action を使用

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Import Secrets from Vault
        uses: hashicorp/[email protected]
        id: vault
        with:
          url: https://vault.example.com:8200
          role: github-actions-role
          method: jwt
          secrets: |
            secret/data/ci/aws accessKey | AWS_ACCESS_KEY_ID ;
            secret/data/ci/aws secretKey | AWS_SECRET_ACCESS_KEY
      
      - name: Deploy with AWS credentials
        run: |
          echo "Accessing AWS with Vault-provided credentials"
          aws s3 ls

このフローでは、vault-action が GitHub が提供する OIDC Token で Vault に自動認証します。Vault が検証後 Secrets を返し、環境変数に注入されます。1 時間後に Token は自動失効し、次回実行時に再取得します。

Azure Key Vault も同様の OIDC 連携をサポートしており、azure/login Action と Azure Key Vault Secrets を組み合わせて設定できます。

GITHUB_TOKEN 権限制御の実践

GITHUB_TOKEN とは

GitHub Actions ワークフローが実行されるたびに、自動的に GITHUB_TOKEN が付与されます。これは一時的な OAuth Token で、現在のリポジトリを操作するために使われます——Release 作成、コードプッシュ、PR へのコメントなど。

問題は、GITHUB_TOKEN のデフォルト権限が大きすぎることです。

旧バージョンの GitHub Actions では、GITHUB_TOKEN の読み書き権限がほぼ全開でした。ワークフローはリポジトリ内容の変更、ブランチ作成、コードプッシュを自由に行えました。ワークフローが攻撃者に悪用された場合(PR トリガーの悪意あるスクリプトなど)、GITHUB_TOKEN は攻撃者の武器になります。

permissions キーの完全設定

GitHub は 2021 年 4 月に permissions キーを導入し、GITHUB_TOKEN の権限範囲を正確に制御できるようにしました。

基本構文は次のとおりです。

permissions:
  actions: read|write|none      # Actions の管理
  contents: read|write|none     # リポジトリ内容
  issues: read|write|none       # Issue 操作
  packages: read|write|none     # GitHub Packages
  pull-requests: read|write|none # PR 操作
  security-events: read|write|none # セキュリティイベント報告
  deployments: read|write|none  # デプロイ状態
  statuses: read|write|none     # Commit 状態

重要な仕組み:ワークフローに permissions キーを書くと、指定していない権限はすべて自動的に none になります。これが「最小権限」のセキュリティ境界です。

ワークフローレベル vs ジョブレベル権限

permissions は 2 つのレベルに書けます。ワークフローレベル(全体)とジョブレベル(局所)です。

ワークフローレベル権限はすべてのジョブに適用されます。

name: CI Pipeline
permissions:
  contents: read    # すべてのジョブはデフォルトでリポジトリ内容を読み取り専用
  issues: write     # すべてのジョブは Issue を書き込み可能

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Lint with read-only access"
  
  test:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Test with read-only access"

ジョブレベル権限はワークフロー設定を上書きできます。

name: Release Pipeline
permissions:
  contents: read    # デフォルトは読み取り専用

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read  # 読み取り専用を継承
    steps:
      - run: echo "Build needs read only"
  
  release:
    runs-on: ubuntu-latest
    permissions:
      contents: write  # 書き込みに上書き、Release 作成用
      packages: write  # Packages へ公開
    steps:
      - name: Create Release
        uses: actions/create-release@v1

実例:あるプロジェクトの Release ワークフローでは、build ジョブはコード読み取りのみ、release ジョブは Release 作成と Docker イメージのプッシュが必要でした。ジョブレベル権限で分離することで、build ジョブが攻撃されてもリポジトリ内容を書き込めません。

リポジトリレベル権限モード

ワークフロー設定以外に、GitHub リポジトリ設定にも 2 つの権限モードがあります。

  • Permissive(寛容):GITHUB_TOKEN はデフォルトですべての権限を読み書き。ワークフローが permissions を指定しない場合に適用。
  • Restricted(制限):GITHUB_TOKEN はデフォルトで contents と packages のみ読み取り。書き込み権限にはワークフローで追加宣言が必要。

すべてのリポジトリを Restricted モードに設定することを推奨します。リポジトリ Settings > Actions > General で「Workflow permissions」を見つけ、「Read repository contents and packages permissions」を選択します。

こうすれば、ワークフローが permissions の記述を忘れても過剰権限にはなりません。セキュリティ境界はリポジトリレベルで既にロックされています。

監査ログとコンプライアンスチェック

ワークフロー実行イベントが監査ログに含まれる

2021 年 2 月から、GitHub は Actions ワークフロー実行イベントを組織監査ログに含めました。誰がどのワークフローを起動し、どの権限を使い、どの Secrets にアクセスしたかを追跡できます。

監査ログの入口:組織 Settings > Security > Audit log。

主要フィールド:

  • action:イベント種別。例:workflow_run.createworkflow_run.complete
  • actor:トリガー者。ユーザー名、App、github-actions[bot] など
  • repo:リポジトリパス
  • token_scopes:使用した Token の権限範囲
  • request_id:リクエスト追跡 ID。ログ関連付けの調査用

実用例:異常なワークフロー実行を発見したとき、監査ログで原因をたどる。例えば Secrets への異常アクセスがあれば、workflow_run イベントからトリガー者を特定できます。

エンタープライズ監査 API

組織が GitHub Enterprise を使っている場合、Audit Log API ですべての操作を照会できます。

curl -H "Authorization: Bearer YOUR_TOKEN" \
  "https://api.github.com/enterprises/YOUR_ENTERPRISE/audit-log?phrase=workflow_run"

返却 JSON には詳細なイベント記録が含まれます。SIEM システム(Splunk、Datadog など)にエクスポートして継続監視できます。

コンプライアンス要件の概要

プロジェクトが SOC 2 や ISO 27001 コンプライアンスを満たす必要がある場合、CI/CD セキュリティは必須項目です。監査ログは最も直接的なコンプライアンス証拠——CI/CD 活動を追跡し、異常を発見し、インシデントに対応できる能力を示します。

推奨設定:監査ログを外部システムにエクスポートし、重要イベントのアラートルールを設定する。例:

  • ワークフロー失敗率の急増
  • Secrets への異常アクセス(短時間での大量読み取り)
  • 未知の IP からのワークフロー起動

GitHub 2026 セキュリティロードマップ展望

GitHub は 2026 年 3 月に Actions セキュリティロードマップを公開し、6 つの主要新機能を計画しました。一部は既にリリース済み、一部は開発中です。

ワークフロー級依存ロック

tj-actions 型攻撃に対する公式ソリューションです。package-lock.json と同様、すべての Action 参照を SHA にロックします。ワークフローでは uses: tj-actions/changed-files@v45 と書いても、ロックファイルが対応する SHA を記録します。メンテナーが Action を更新しても、ロックファイルは自動では変わりません——能動的にレビューして更新する必要があります。

この機能はまだリリースされていませんが、今から手動で SHA 固定ができます。

Layer 7 ネイティブアウトバウンドファイアウォール

CI/CD フローの外部ネットワークアクセスをネイティブ制御。例えばワークフローが AWS API のみにアクセスでき、任意の外部サイトにはアクセスできないように制限。

現状これを実現するには、セルフホスト Runner + カスタムネットワークポリシーが必要です。2026 年リリース後は、GitHub クラウド Runner でもアウトバウンドファイアウォールを設定できます。

Scoped Secrets

より細かい Secrets スコープ制御。例えば特定の Secrets を特定ブランチや特定ジョブのみがアクセス可能に。現状 Secrets のスコープはリポジトリレベルまたは環境レベルで、粒度が粗いです。

ポリシー駆動実行制御

信頼境界、承認、証明ゲートを定義。例えば fork 由来の PR はすべて人手承認後にのみワークフローを起動、またはワークフローがセキュリティスキャンを通過しないと実行不可、といった制御。

環境保護ルールに似ていますが、粒度が細かく、ロジックも複雑です。

Actions Data Stream

CI/CD 活動のリアルタイム可視性。監査ログに似ていますが、事後照会ではなくリアルタイムプッシュ。SIEM システムに接続してリアルタイム監視できます。

OIDC カスタム属性クレーム

クラウドプロバイダー認証の強化。OIDC Token にリポジトリタグ、ブランチ情報などのカスタム属性を載せられます。Vault や AWS がこれらの属性に基づき、より細かい認可判断が可能になります。

まとめ

tj-actions 事件から今に至るまで、学んだ教訓は 3 点です。

第一、SHA 固定。 サードパーティ Action をタグ参照しない。完全な SHA を使う。23,000 リポジトリが痛い代償を払った教訓です。

第二、最小権限。 GITHUB_TOKEN のデフォルト権限は大きすぎる。permissions キーで制限し、リポジトリを Restricted モードに設定する。

第三、監査ログ。 Actions イベントは既に監査対象。異常実行を定期的に確認し、監視システムへエクスポートする。

この 3 点は今すぐ設定できます。GitHub 2026 の新機能を待つ必要も、新しいツールに乗り換える必要もありません。リポジトリを開き、Action 参照、権限設定、監査ログを確認しましょう。30 分で基本防御は完了します。

結局、CI/CD セキュリティは高度な技術ではなく、細部の習慣の問題です。SHA 固定が 1 つ増えるたび、過剰権限が 1 つ減るたび、リスクは少しずつ下がります。tj-actions 事件が教えてくれたのは、攻撃者に高度な手口は不要で、私たちが 1 つの小さな細部を見落とすだけで十分、ということです。

GitHub Actions セキュリティ防御の設定フロー

SHA 固定から権限制御まで、CI/CD セキュリティ強化の 3 つの核心ステップを完了する。

⏱️ 目安時間: 30 分

  1. 1

    ステップ 1: Action 参照を SHA で固定

    すべてのワークフローファイルを確認し、`uses: action@tag` を `uses: action@full-sha` に変更してタグ改ざんを防ぐ。`pinact` などのツールで既存参照を一括変換する。
  2. 2

    ステップ 2: `permissions` キーを設定

    ワークフローまたはジョブレベルに permissions キーを追加し、必要な権限のみ付与する。例:`permissions: contents: read` は読み取り専用操作、`permissions: contents: write` は Release 作成用。リポジトリを Restricted モードに設定して兜底する。
  3. 3

    ステップ 3: 監査ログモニタリングを有効化

    組織 Settings > Security > Audit log で Actions ワークフロー実行イベントを確認する。重要イベントのアラートを設定:ワークフロー失敗率の急増、Secrets への異常アクセス、未知の IP からのワークフロー起動。ログを SIEM システムにエクスポートして継続監視する。

FAQ

なぜタグ参照ではなく SHA 固定を使うのか?
タグは可変です。リポジトリメンテナーや攻撃者がいつでもタグを新しいコミットに向け直せます。SHA は固定され、参照を更新しない限り同じコードが実行されます。tj-actions 事件がタグ改ざんのリスクを証明しました。
GITHUB_TOKEN の権限はどう制御する?
`permissions` キーで必要な権限を正確に宣言します。permissions を書くと、指定していない権限は自動的に none になります。リポジトリ Settings > Actions > General で権限モードを Restricted に設定することを推奨します。
ワークフローが攻撃されたかどうかはどう検知する?
組織監査ログの workflow_run イベントを確認し、異常アクセス、未知の IP、高頻度の失敗などに注目します。ログを SIEM システムにエクスポートして自動アラートを設定します。
OIDC + Vault は組み込み Secrets と比べて何が優れている?
GitHub 組み込み Secrets は長期資格情報で、リポジトリが侵害されるとすべて漏洩します。OIDC + Vault は資格情報なしアクセスを実現し、GitHub が Vault に身元を証明して短期 Token を取得します。1 時間後に自動失効し、攻撃ウィンドウが大幅に縮小します。
サードパーティ Action は使える?
使えますが、慎重に。公式 Action を優先し、SHA 固定参照を使い、依存関係を定期的に監査してください。GitHub 2026 ロードマップではワークフロー級依存ロック機能が登場予定で、より安全にサードパーティ Action を管理できます。

8分で読めます · 公開日: 2026年5月16日 · 更新日: 2026年7月9日

コメント

GitHubアカウントでログインしてコメントできます

Easton BlogEaston Blog