GitHub Actionsが攻撃者に狙われる構造的な理由

GitHub Actionsのワークフローは、リポジトリのソースコード・クラウドのデプロイ鍵・npmやDockerのパブリッシュトークンなど、ソフトウェアサプライチェーンの中核となる資産へのアクセス権を持っています。攻撃者にとっては、ひとつのワークフローを侵害するだけで、ビルド成果物の改ざん、本番環境の認証情報の窃取、下流プロジェクトへの連鎖感染が一気に可能になります。

2025年3月に発覚したtj-actions/changed-filesのサプライチェーン攻撃では、23,000以上のリポジトリが影響を受け、CI実行時のメモリからGitHub PAT・npmトークン・RSA秘密鍵などが窃取されました(出典: Wiz Blog)。CISAもCVE-2025-30066として緊急アラートを発行しています(出典: CISA)。

CI/CDパイプラインはファイアウォールの内側にあるという誤解が根強いものの、GitHub-hostedランナーはパブリックなインフラ上で動作しており、ワークフローファイル自体がリポジトリ内のYAMLとして可視化されています。攻撃者はソースコードを読むだけで攻撃面を特定できるため、Webアプリケーションと同等以上のセキュリティ設計が求められます。

攻撃手法の体系的分類 – Poisoned Pipeline Execution(PPE)

OWASPの「CI/CD Security Top 10」では、CI/CDパイプラインを悪用する攻撃を Poisoned Pipeline Execution(PPE) として体系化しています(出典: OWASP)。攻撃者の介入経路によって3種類に分類されます。

Direct PPE(D-PPE): ワークフロー定義の直接改ざん

攻撃者がリポジトリへの書き込み権限を持ち、.github/workflows/配下のYAMLファイルを直接変更するパターンです。保護されていないブランチへのpushや、レビューなしでマージ可能なPull Requestを経由して悪意あるステップを追加します。

典型的なシナリオ:

  • 退職者アカウントが残存し、ブランチ保護なしの環境でワークフローを書き換える
  • フォーク元リポジトリのメンテナがワークフローに悪意あるコードを混入する

Indirect PPE(I-PPE): ワークフローが参照するファイルの改ざん

ワークフロー定義は保護されていても、そこから呼び出されるスクリプトやテストコード、設定ファイルが改ざん対象になるパターンです。Makefileやテストフレームワークの設定ファイルへの変更がCI上で自動実行されることを悪用します。

典型的なシナリオ:

  • Makefile内のターゲットにcurl | bash形式の外部コマンドを仕込む
  • テストフレームワークのconftest.pyやsetupスクリプトに情報窃取コードを追加する

Public PPE(3PE): OSSへのPull Requestを利用した攻撃

パブリックリポジトリに対して匿名ユーザーがPull Requestを送り、pull_request_targetトリガーなどで特権付きワークフローを実行させるパターンです。

典型的なシナリオ:

  • OSSリポジトリに悪意あるPRを送信し、pull_request_target経由でSecretsを含む環境でコードを実行させる
  • PRのタイトルやボディにインジェクションペイロードを埋め込み、ワークフロー内で展開させる

PPE以外の主要な攻撃経路

攻撃経路手法の概要影響範囲
サプライチェーン攻撃サードパーティアクションのタグ書き換え・リポジトリ乗っ取り当該アクションを使う全リポジトリ
スクリプトインジェクション${{ github.event.* }}の未サニタイズな展開当該ワークフローのジョブ
認証情報の残存actions/checkoutのデフォルト設定による.git/configへの資格情報書き込み成果物をアップロードする全ジョブ
Secretsのログ漏洩構造化データ(JSON/YAML)をSecretsに格納した場合のマスク失敗パブリックリポジトリのログ

実際に起きたインシデント: tj-actions/changed-files連鎖攻撃の全容

2025年3月のtj-actions事件は、GitHub Actionsのサプライチェーン攻撃としては過去最大規模のものでした。時系列と攻撃の連鎖構造を整理します。

攻撃の連鎖

  1. 起点: 攻撃者がまずreviewdog/action-setup@v1のリポジトリを侵害(CVE-2025-30154)
  2. 中継: reviewdogの侵害を足がかりに、tj-actions/changed-filesのメンテナ権限を取得
  3. 拡散: tj-actions/changed-filesのv1.0.0からv44.5.1まで、全バージョンタグを単一の悪意あるコミットに書き換え
  4. 実行: 悪意あるNode.jsコードがBase64エンコードされた命令を実行し、Pythonスクリプトをダウンロード
  5. 窃取: ランナーのメモリをスキャンしてCI実行時の全Secretsを収集、ワークフローログにBase64二重エンコードで出力

出典: Palo Alto Networks Unit 42

被害の特徴

パブリックリポジトリでは、ワークフローログが一般公開されているため、窃取された認証情報は二重Base64デコードするだけで誰でも読める状態でした。影響を受けた期間は2025年3月12日00:00 UTCから3月15日12:00 UTCの約3日間で、この間に当該アクションを実行した全リポジトリが対象です。

StepSecurity社のHarden-Runnerが異常なネットワーク通信を検知し、最初の公開アラートを発したことで事態が発覚しました(出典: StepSecurity)。

この事件から学ぶべきこと

  • バージョンタグ(v1, v2など)は可変参照であり、攻撃者が書き換え可能です
  • 1つのアクションの侵害が連鎖的に他のアクションへ波及します
  • ランナー上のメモリは全てのSecretsを含んでおり、任意コード実行が可能になった時点で全Secretsが危殆化します

防御策の実践(優先度順)

1. permissionsの最小権限化

GitHub Actionsのワークフローが持つGITHUB_TOKENの権限は、デフォルトではリポジトリの設定に依存します。Organization設定またはリポジトリ設定でデフォルトを読み取り専用に変更し、各ワークフローで必要な権限だけを明示的に宣言する運用が最優先です。

# ワークフローレベルで全権限をnoneに設定
permissions: {}

jobs:
  lint:
    runs-on: ubuntu-latest
    # ジョブ単位で必要最小限を付与
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write  # OIDC認証に必要
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

ワークフローのトップレベルでpermissions: {}と記述すると、全ジョブのデフォルト権限がnoneになります。各ジョブは自分に必要な権限だけをpermissionsブロックで宣言します。ジョブレベルの権限はワークフローレベルを超えられない(下方修正のみ)ため、万が一あるジョブが侵害されても、他のジョブの権限には影響しません。

2. サードパーティアクションのSHAピン留め

バージョンタグ(v1, v2.3.1など)は可変参照であり、リポジトリオーナーがいつでも別のコミットを指すように書き換えられます。tj-actions事件はまさにこの仕組みを悪用したものでした。コミットSHAの完全ハッシュでピン留めすることで、参照先のコードが変更されないことを保証します。

# NG: タグは書き換え可能
- uses: actions/checkout@v4

# OK: SHAピン留め + コメントでバージョンを明記
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

手動でSHAを管理するのは現実的ではないため、ツールによる自動化が必須です。

  • pinact: タグ参照をSHAピン留めに一括変換するCLIツールです。pinact runでリポジトリ内の全ワークフローを変換でき、コメントに元のバージョン番号を残します(出典: GitHub suzuki-shunsuke/pinact
  • Dependabot / Renovate: ピン留めしたSHAを新しいバージョンのSHAに自動更新するPull Requestを生成します。ピン留めと更新の自動化を組み合わせることで、セキュリティと利便性を両立できます

3. スクリプトインジェクションの防止

${{ github.event.pull_request.title }}${{ github.event.issue.body }}などのイベントデータは、攻撃者が自由にコントロールできる入力値です。これをrun:ブロック内で直接展開すると、OSコマンドインジェクションが成立します。

# 脆弱なパターン: PRタイトルがシェルコマンドとして解釈される
- run: echo "PR Title: ${{ github.event.pull_request.title }}"

# 安全なパターン: 中間環境変数を経由させる
- env:
    PR_TITLE: ${{ github.event.pull_request.title }}
  run: echo "PR Title: ${PR_TITLE}"

中間環境変数を使う理由は、${{ }}構文はYAMLテンプレートとして文字列置換されるため、シェルのクォートをバイパスできてしまうのに対し、環境変数として渡せばシェルが通常の変数展開として処理するためです。

4. actions/checkoutの認証情報残存を防ぐ

actions/checkoutはデフォルトでpersist-credentials: trueが設定されており、チェックアウト後のリポジトリの.git/configにGITHUB_TOKENの認証情報が残存します。後続のステップでgit pushなどを実行できるようにする意図ですが、アーティファクトのアップロードやキャッシュの保存時にこの認証情報が意図せず漏洩するリスクがあります(出典: actions/checkout Issue #485)。

- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
  with:
    persist-credentials: false

この設定を追加すると、チェックアウト完了後に認証情報がクリーンアップされます。後続ステップでgit操作が必要な場合は、そのステップ内で明示的にトークンを設定してください。

5. Secrets管理の落とし穴: 構造化データのマスク制限

GitHub ActionsのSecretsマスク機能は、ログ出力時にSecretの値と完全一致する文字列を***に置換する仕組みです。JSON・XML・YAMLなどの構造化データをひとつのSecretに格納すると、整形や部分抽出の過程で元の文字列と一致しなくなり、マスクが失敗します(出典: GitHub Docs)。

# 危険: JSONをそのままSecretsに格納
# secrets.SERVICE_ACCOUNT = '{"client_id":"xxx","client_secret":"yyy"}'
# → jqで部分抽出するとclient_secretがログに平文表示される

# 安全: 値を個別のSecretsに分割
# secrets.CLIENT_ID = 'xxx'
# secrets.CLIENT_SECRET = 'yyy'

対策のポイント:

  • Secretsには必ず単一の値(文字列・トークン・パスワード)のみを格納します
  • 複数の認証情報が必要な場合は、それぞれを個別のSecretとして登録します
  • ランタイムで生成した機微値は::add-mask::コマンドで手動マスクを追加します

6. OIDC連携によるSecretsレス認証

クラウドプロバイダへのデプロイに長期間有効なアクセスキーをSecretsに格納するのは、漏洩時の被害が大きく、ローテーション運用も煩雑です。GitHub ActionsのOIDC(OpenID Connect)機能を使うと、ワークフロー実行時に短命のIDトークンを発行し、クラウド側のIAMロールと連携して一時的な認証情報を取得できます(出典: GitHub Docs)。

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
        with:
          role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsDeployRole
          aws-region: ap-northeast-1

OIDC連携の利点:

  • リポジトリにAWSアクセスキーを保存する必要がなくなります
  • トークンはジョブ単位で発行され、ジョブ終了後に自動失効します
  • クラウド側のIAMポリシーで、特定のリポジトリ・ブランチ・環境からのみアクセスを許可する条件を設定できます

AWS・GCP・Azureの主要3クラウドが全て対応しており、HashiCorp Vaultとの組み合わせによるマルチクラウドシークレット管理も実現可能です。

ワークフローを検査するセキュリティツール

静的解析ツールを組み合わせることで、レビュー漏れや設定ミスを機械的に検出できます。主要なツールの特徴を比較します。

ツール主な検査対象検出できる問題の例導入方法
actionlintYAMLの構文・型・式式のインジェクションリスク、存在しないイベント参照、マトリクスの型不整合go installbrew、バイナリ配布
ghalintセキュリティポリシータグ参照(SHA未使用)、permissions未宣言、read-all/write-all指定aqua、バイナリ配布
pinactアクションバージョン参照ミュータブルなタグ参照の検出とSHAピン留めへの自動変換go installaqua
zizmorセキュリティ脆弱性テンプレートインジェクション、既知の脆弱なアクション、権限過剰設定pipcargouv

出典: zizmor GitHub

actionlintの導入例

jobs:
  workflow-lint:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
      - uses: reviewdog/action-actionlint@a5524e1de28532e3aab3da013907e785d1af36db # v1.62.0
        with:
          reporter: github-pr-review

actionlintは構文エラーだけでなく、${{ github.event.* }}の危険な展開パターンも検知します。Pull Requestのレビューコメントとして指摘を出力する設定にすると、セキュリティレビューの自動化が実現できます。

zizmorの活用

zizmorはGitHub Actions専用のセキュリティスキャナーで、SARIF形式で結果を出力できるため、GitHubのSecurity Alertsタブに直接統合できます(出典: Grafana Labs Blog)。

# インストール
pip install zizmor

# リポジトリの全ワークフローをスキャン
zizmor .github/workflows/

# SARIF形式で出力(GitHub Code Scanningと統合可能)
zizmor --format sarif .github/workflows/ > results.sarif

actionlintが「構文の正しさ」を担保し、zizmorが「セキュリティの正しさ」を担保するという役割分担で、CI上で両方を実行する構成が効果的です。

ランタイム監視: Harden-Runnerによるネットワーク制御

静的解析だけでは、ビルド中に動的にダウンロードされるスクリプトや、アクションの内部で行われる予期しないネットワーク通信を検出できません。StepSecurity社のHarden-Runnerは、GitHubランナー上でEDR(Endpoint Detection and Response)のように動作し、ランタイムの挙動を監視します(出典: GitHub step-security/harden-runner)。

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
        with:
          egress-policy: audit  # まずは監査モードで通信先を記録
          # egress-policy: block  # 本番運用ではブロックモードに切り替え
          # allowed-endpoints: >
          #   github.com:443
          #   registry.npmjs.org:443
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
      - run: npm ci && npm test

導入の流れ:

  1. egress-policy: auditで通信先を収集し、正常なエンドポイント一覧を作成します
  2. 一覧をallowed-endpointsに設定し、egress-policy: blockに切り替えます
  3. 許可リスト外への通信はブロックされ、StepSecurityのダッシュボードにアラートが記録されます

tj-actions事件では、Harden-Runnerが攻撃者のC2サーバーへの異常な通信を検知して第一報を発しています。静的解析が「予防」であるのに対し、ランタイム監視は「検知と遮断」を担う多層防御の重要な要素です。

組織レベルのセキュリティポリシー

個々のリポジトリでの対策に加え、Organization全体でポリシーを強制する仕組みが必要です。

Organization Rulesets

GitHub Organization Rulesetsは、従来のBranch Protection Rulesを拡張し、Organization内の複数リポジトリに対して一括でルールを適用できる機能です。2025年6月にはGitHub Teamプランでも利用可能になりました(出典: GitHub Changelog)。

セキュリティ観点で有効な設定:

  • .github/workflows/ディレクトリの変更に対してCODEOWNERSレビューを必須にする
  • ステータスチェック(actionlint、zizmorのCI)の通過を必須にする
  • force pushの禁止

Environment Secrets + Deployment Protection Rules

本番環境へのデプロイに使うSecretsは、リポジトリSecretsではなくEnvironment Secretsに格納し、Deployment Protection Rulesで保護します。

jobs:
  deploy-production:
    runs-on: ubuntu-latest
    environment:
      name: production  # 承認者のレビューが必要な環境
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
        with:
          persist-credentials: false

Environment Secretsは指定された環境でのみ参照可能であり、Deployment Protection Rulesにより指定されたレビュアーの承認がないとジョブが開始されません。これにより、仮にワークフローが改ざんされても、本番用のSecretsにアクセスするには人間の承認が必要になります。

CODEOWNERSによるワークフロー変更の管理

# .github/CODEOWNERS
/.github/workflows/  @security-team
/.github/actions/    @security-team

CODEOWNERSファイルでワークフローディレクトリの所有者をセキュリティチームに設定し、Branch Protection RulesでCODEOWNERSレビューを必須化します。ワークフローの変更がセキュリティチームの承認なしにマージされるのを防止できます。

Reusable Workflowsによる標準化

組織で共通のセキュリティ対策(権限設定、Harden-Runner、actionlint)を組み込んだReusable Workflowを提供し、各リポジトリから呼び出す形にすると、ポリシーの一貫性を保てます。

# org/.github/workflows/secure-build.yml
on:
  workflow_call:
    inputs:
      node-version:
        type: string
        default: '20'

permissions: {}

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
        with:
          egress-policy: audit
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
        with:
          persist-credentials: false
      - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
        with:
          node-version: ${{ inputs.node-version }}
      - run: npm ci && npm test

Reusable Workflowで重要なのは、呼び出し元(caller)から渡されるGITHUB_TOKENの権限は下方修正のみ可能で、呼び出し先で権限を昇格できない点です。これにより、共通ワークフローが不正に高い権限で実行されるリスクを排除できます。

ワークフローが侵害された場合のインシデントレスポンス

予防策を講じていても侵害が発生する可能性はゼロにはなりません。侵害発生時の対応手順をあらかじめ整備しておくことが重要です。

即時対応(発覚から1時間以内)

  1. 影響範囲の特定: ワークフロー実行ログを確認し、不審なステップ・コマンドの有無を調査します。侵害されたアクションのバージョンとそれを使用しているリポジトリの一覧を作成します
  2. ワークフローの停止: 該当するワークフローを無効化するか、侵害されたアクションへの参照を削除したコミットをpushします
  3. Secretsの全ローテーション: 侵害期間中にワークフローがアクセスした可能性のある全てのSecrets(GITHUB_TOKEN以外)を即座にローテーションします。GITHUB_TOKENはジョブ終了時に自動失効しますが、ログに出力されたPATやAPIキーは手動対応が必要です

調査フェーズ(1時間〜24時間)

  1. ログの保全: ワークフロー実行ログ、監査ログ(Organization設定 > Audit log)をエクスポートして保全します。パブリックリポジトリの場合、攻撃者がログを先に取得している前提で対応します
  2. 窃取された認証情報の悪用調査: 各クラウドプロバイダのCloudTrail(AWS)、Cloud Audit Logs(GCP)、Activity Log(Azure)で、窃取された可能性のある認証情報による不正アクセスがないか確認します
  3. 影響を受けたアーティファクトの特定: 侵害期間中にビルド・パブリッシュされたDockerイメージ、npmパッケージ、その他のアーティファクトを特定し、改ざんの有無を検証します

再発防止(24時間〜1週間)

  1. 全リポジトリのアクション参照を棚卸し: SHAピン留めされていないアクション参照をpinactで一括変換します
  2. permissions の棚卸し: 全ワークフローで明示的なpermissions宣言が行われているか確認し、未宣言のものを修正します
  3. ランタイム監視の導入: Harden-Runnerなどのランタイム監視が未導入であれば、まずauditモードで導入します
  4. ポストモーテムの実施: 侵害の根本原因、検知までの時間、対応の課題を文書化し、組織内で共有します

セキュリティチェックリスト

日々の開発で参照できるチェックリストです。

必須(全リポジトリで即時対応)

  • ワークフローのトップレベルにpermissions: {}を設定し、ジョブ単位で最小権限を宣言している
  • 全てのサードパーティアクションがコミットSHAでピン留めされている
  • ${{ github.event.* }}の値をrun:で直接展開していない(中間環境変数を使用)
  • actions/checkoutpersist-credentials: falseを設定している
  • SecretsにJSON/XML/YAMLなどの構造化データを格納していない

推奨(段階的に導入)

  • Dependabot / Renovateでアクションの自動更新PRを生成している
  • actionlint + zizmorをCIで実行し、Pull Requestごとに検査している
  • 本番デプロイ用のSecretsはEnvironment Secretsに移行し、Deployment Protection Rulesを設定している
  • OIDC連携でクラウドプロバイダへの長期認証情報をSecretsから排除している
  • .github/workflows/ディレクトリに対するCODEOWNERSが設定されている
  • Harden-Runnerをauditモード以上で導入している

上級(大規模組織向け)

  • Organization Rulesetsで全リポジトリにワークフロー変更のレビュー必須ルールを適用している
  • Reusable Workflowで組織標準のセキュリティ設定を共通化している
  • Harden-Runnerのblockモードを有効化し、許可リスト外の通信を遮断している
  • インシデントレスポンス手順書が文書化され、定期的に訓練を実施している

学習リソース

GitHub Actionsのセキュリティを体系的に学習できるリソースを紹介します。

  • GitHub Actions Goat: StepSecurity社が提供する、意図的に脆弱性を含んだGitHub Actions環境です。フォークして実際に攻撃と防御を体験できます(出典: GitHub step-security/github-actions-goat
  • OWASP CI/CD Security Top 10: CI/CDパイプライン固有のセキュリティリスクを体系化したガイドラインです。PPE以外にも「不十分なPBAC(Pipeline-Based Access Controls)」「依存チェーンの悪用」など10カテゴリを網羅しています(出典: OWASP
  • MITRE ATT&CK T1677: MITREのATT&CKフレームワークにPoisoned Pipeline Executionが追加されています。攻撃テクニックの詳細と検知方法が記述されています(出典: MITRE ATT&CK
  • GitHub公式セキュリティガイド: GitHubが公式に提供するワークフローのセキュリティベストプラクティスです(出典: GitHub Docs

GitHub Actionsのセキュリティは、ワークフローYAMLの記述だけでなく、サプライチェーン全体の信頼性を設計する問題です。静的解析による予防、ランタイム監視による検知、インシデントレスポンスによる対応の3層で防御を構築し、組織のCI/CDパイプラインを保護してください。