GitHub Actionsのジョブは、シークレットを盗むには格好の場所です。デプロイキー、レジストリのトークン、クラウドの認証情報を保持し、さらに誰かがほんの1分前に書いたコード、つまりサードパーティのアクション、パッケージ、そのインストールスクリプトを実行します。GitHubのシークレットの扱いは、うっかりミスからは守ってくれます。しかし、すでにジョブ内で実行されているコードを止めるようには設計されていません。
この記事では、GitHub Actionsでシークレットがどのように機能するか、実際に漏えいする5つの経路、そしてそれぞれを断つための対策を解説します。
GitHub Actionsのシークレットの仕組み
シークレットは、組織、リポジトリ、環境(Environment)の3つのレベルに置かれます。同じ名前が複数のレベルに存在する場合は、最も範囲の狭いものが優先されます。環境はリポジトリより、リポジトリは組織より優先されます。
知っておくべき上限とルールがいくつかあります。
- サイズと数。組織のシークレットは最大1,000個、リポジトリのシークレットは100個、環境のシークレットは100個で、それぞれ最大48 KBです。ワークフローからはリポジトリと環境のシークレットはすべて見えますが、組織のシークレットはアルファベット順で最初の100個しか見えません。
- フォークには何も渡されない。
GITHUB_TOKENを除き、フォークからのプルリクエストによってトリガーされたワークフローにはシークレットが渡されません。 - Dependabotはフォークと同様に扱われる。Dependabotによってトリガーされたワークフローは、読み取り専用のトークンとDependabotのシークレットのみを受け取り、Actionsのシークレットは受け取りません。
- 再利用可能なワークフローにはシークレットを渡す必要がある。
secrets: inheritは、同じ組織またはエンタープライズ内で、すべてのシークレットを暗黙的に渡します。環境のシークレットはworkflow_call経由では渡せません。 - 環境がアクセスを制御する。必須レビュアーを設定すると、誰かが承認するまでジョブはその環境のシークレットを読み取れません。デプロイブランチのルールにより、その環境をそもそも使えるブランチが制限されます。
マスキングにできること、できないこと
GitHubはログからシークレットの値を伏せ字にしますが、この伏せ字処理は完全一致を見つけることに依存しています。GitHub自身のドキュメントには、うまく機能しないケースが挙げられています。
- JSONやYAMLなどの構造化データ内のシークレット
- トークンを別のトークンに交換した場合など、シークレットから派生した値(
::add-mask::で登録しない限り) - base64エンコードなど、変換されたシークレット
マスキングは、シークレットを誤って出力してしまうことへの防御策です。先にシークレットをエンコードするコードに対しては何の効果もありません。
ワークフローからシークレットが漏えいする経路
1. 侵害されたアクションがシークレットを出力する
2025年3月、攻撃者は23,000を超えるリポジトリで使われているアクションtj-actions/changed-files(CVE-2025-30066)を侵害しました。アクションの既存のバージョンタグを悪意のあるコミットに付け替えたため、@v45などのタグを参照していたすべてのワークフローで攻撃者のコードが実行されました。安全だったのは、完全なコミットSHAで固定していたワークフローだけです。
ペイロードはランナーのメモリからシークレットを読み出し、マスキングに検出されないよう2回base64エンコードしてジョブログに出力しました。パブリックリポジトリでは、そのログは誰でも読めます。この侵害は別のアクションであるreviewdog/action-setup(CVE-2025-30154)から始まり、さらにその前にはpull_request_targetワークフローを通じて盗まれた個人アクセストークンがありました。
2. pull_request_targetワークフローが信頼できないコードを実行する
pull_request_targetは、プルリクエストがフォークからのものであっても、ベースリポジトリのコンテキストで、そのシークレットと書き込み可能なトークンを持って実行されます。これが安全なのは、ワークフローがプルリクエストのコードを一切実行しない場合に限られます。ワークフローがプルリクエストのheadをチェックアウトしてビルドすると、プルリクエストを作成できる人なら誰でも、そのワークフローが持つすべてのシークレットを読み取れてしまいます。GitHub Security Labはこれを「pwn requests」と呼んでいます。
GitHubはこの範囲を狭めてきました。2025年12月以降、pull_request_targetは常にデフォルトブランチのワークフローファイルを使用します。2026年7月以降、actions/checkoutは既知の危険なパターンを使うpull_request_targetワークフローでフォークのコードをチェックアウトすることを拒否します。また、2026年9月に一般提供が始まったワークフロー実行保護により、2026年11月2日から、イベントポリシーのないパブリックリポジトリではpull_request_targetがデフォルトで無効になります。
3. 信頼できない入力がスクリプトに注入される
ワークフローの式は、シェルが実行される前に展開されます。次のようなステップは、
- run: echo "Title: ${{ github.event.issue.title }}"
Issueのタイトルに含まれているものを何でも実行します。攻撃者がシェルコマンドをタイトルにしたIssueを作成すると、ジョブはそれを実行し、ジョブのシークレットに手が届いてしまいます。信頼できない値は代わりに環境変数を通して渡し、シェルがそれをデータとして扱うようにしてください。
- env:
TITLE: ${{ github.event.issue.title }}
run: echo "Title: $TITLE"
4. キャッシュと成果物がシークレットを持ち出す
- 成果物。2024年、Unit 42のArtiPACKEDの調査により、
actions/checkoutはデフォルトでGITHUB_TOKENを.git/configに書き込むため、ワークスペースを成果物としてアップロードすると、それをダウンロードできる誰にでもトークンが漏えいすることが示されました。upload-artifactは2024年9月以降デフォルトで隠しファイルを除外し、actions/checkoutv6は認証情報をチェックアウトディレクトリの外に保存しますが、固定された古いバージョンや独自のアップロードステップでは依然として漏えいします。 - キャッシュ。リポジトリのいずれかのワークフローでコードを実行できる攻撃者は、そのキャッシュを埋め尽くして本物のエントリを追い出し、同じキーで汚染したエントリを仕込むことができます。後で実行される特権を持つワークフローがそれを復元します。2026年5月には、
pull_request_targetから始まり、ランナーのメモリから読み取ったOIDCトークンで終わるこの一連の手口によって、攻撃者はTanStackの42パッケージについて84の悪意のあるバージョンを公開しました。
5. 依存関係がシークレットをサーバーに送信する
インストールスクリプトは、ジョブのほかの部分と同じ権限で実行されます。2025年9月にnpmで初めて確認されたShai-Huludワームは、npm installの実行中に動作し、環境からnpmトークン、GitHubトークン、クラウドのキーを収集して、パブリックのGitHubリポジトリに投稿しました。2025年11月の波ではpreinstallの段階で実行され、感染したマシンをセルフホストランナーとして登録したため、攻撃者は後から戻ってくることができました。
この経路では、ワークフロー側にミスがなくても攻撃が成立します。依存関係ツリーのどこかに侵害されたバージョンが1つあれば十分で、シークレットはネットワーク経由で外部に流出します。
シークレットを守る方法
保存されたクラウドのキーをOIDCに置き換える。AWS、Azure、Google Cloudのキーをシークレットとして保持する代わりに、ジョブがGitHubのOIDC発行者から短期間有効なトークンを要求し、それをクラウドプロバイダーと交換するようにします。どのリポジトリ、ブランチ、環境がそのロールを引き受けられるかは信頼ポリシーが決定し、認証情報はジョブとともに失効します。ジョブにはpermissions: id-token: writeが必要です。TanStackの事例が示したように、OIDCトークンも実行中のジョブから盗まれる可能性はありますが、盗まれたトークンの価値は、期限切れにならないキーよりはるかに低くなります。
アクションを完全なコミットSHAで固定する。GitHubのドキュメントでは、これがアクションを不変のリリースとして使う唯一の方法とされています。タグは付け替えられますが、SHAは変えられません。2025年8月以降、許可されたアクションのポリシーによって、組織全体でSHAによる固定を強制し、特定のアクションをブロックできるようになりました。DependabotとRenovateは、固定したSHAを最新の状態に保てます。
GITHUB_TOKENには必要最小限の権限を与える。デフォルトを読み取り専用にし、書き込みスコープはジョブごとに付与します。新しい組織とリポジトリは2023年2月以降デフォルトで読み取り専用ですが、それ以前のものは誰かが変更しない限り、権限の広いデフォルトのままです。GITHUB_TOKENの権限:すべてのワークフローに最小権限ををご覧ください。
シークレットを必要なステップに限定する。ステップのenv:に設定したシークレットは、そのステップの環境にしか存在しません。ジョブレベルで設定したシークレットは、すべてのサードパーティのアクションを含む、すべてのステップから見えます。
本番のシークレットは環境の背後に置く。必須レビュアーとデプロイブランチのルールにより、フィーチャーブランチ上のワークフローや、プルリクエストによってトリガーされたワークフローはそれらを読み取れなくなります。
pull_request_targetと信頼できない入力はコード実行として扱う。必要がない限りpull_request_targetは避け、その中でプルリクエストのコードをビルドしないでください。イベントデータは環境変数を通して渡します。CodeQLはワークフローのインジェクションやpwn requestのパターンをチェックでき、GitHub Actions向けには2025年4月から一般提供されています。
シークレットスキャンとプッシュ保護を有効にする。これらはリポジトリにコミットされたシークレットを検出します。上記とは異なる種類の漏えいですが、同じくらいよく起こります。
ジョブの接続先を制限する。ほとんどのジョブに必要な接続先は、パッケージレジストリ、コンテナレジストリ、いくつかのAPIといった短いリストです。ジョブがそれらにしかアクセスできなければ、悪意のあるコードがシークレットを読み取っても送り先がありません。エグレスポリシーでGitHub Actionsからの持ち出しを防ぐ方法をご覧ください。
漏えいの可能性があればローテーションする。ワークフローが侵害されたアクションやパッケージを実行した場合、読み取れたすべてのシークレットが盗まれたと想定してください。使われたと思われるものだけでなく、すべてを失効させて再発行します。
CRACIの役割
CRACIはGitHub Actionsのジョブを自社のランナーで実行します。runs-onをcraciに変更するだけで、ワークフロー、シークレット、実行履歴はGitHubに残ります。CRACIの2つの機能は、上記の漏えいへの対策です。
- フェイルクローズのエグレスポリシー。ジョブがアクセスできるパッケージソース、レジストリ、ホストを設定します。ポリシーはジョブの開始前に検証され、ランナーで適用されるため、それ以外への接続はブロックされ、ポリシー違反時にはアラートが届きます。
- ジョブが行ったことの記録。CRACIは、キャッシュから復元されたパッケージを含め、各ジョブが実際に取得したすべてのパッケージと、ジョブのネットワーク接続のトレースを記録します。次に侵害されたパッケージが公表されたとき、ロックファイルから推測するのではなく、どのビルドがそれを取り込み、どこに接続したかを確認できます。
各ジョブは専用の仮想マシンで実行されるため、あるジョブが残したものが次のジョブに引き継がれることはありません。CRACIはコード内のシークレットをスキャンしません。その用途にはGitHubのシークレットスキャンを引き続きお使いください。
デモを予約して、自社のビルドの記録をご覧ください。
