「CI/CD から AWS にデプロイしたい」「GitHub Actions から GCP にイメージを push したい」——要件は一行です。でも、その実装を AWS_SECRET_ACCESS_KEY や サービスアカウント鍵(JSON)を GitHub Secrets に貼り付けて済ませた瞬間、あなたのリポジトリは 「漏れたら終わり」の長期キーを抱えた攻撃面 に変わります。
この記事は、GitHub Actions のCI/CDから 長期クラウド資格情報を完全に廃止する ための実装ガイドです。鍵を保存する代わりに、OIDC federation で「各ワークフロー実行ごとに短命の署名付きIDトークンを発行 → クラウド側が『どのrepo/branch/environmentからの実行か』を検証して一時クレデンシャルを払い出す」構成に置き換えます。鍵は、どこにも存在しません。
題材として、私が実際に AWS と GCP の両方 で鍵レスCI/CDを運用している構成を交えます。国内大手放送事業者向けの社内AIプラットフォーム(Workload Identity Federation で鍵レスCI/CDを実現)では GCP 側を、別の木材業界DX案件では AWS 側を、それぞれ OIDC で組んでいます。
この記事のルール:信頼できる情報源は GitHub / AWS / GCP の公式ドキュメント(2026年6月時点) のみです。トークンのクレーム名・アクションの入力名・信頼ポリシーのキーは公式から引用し、推測は明示します。クラウドの仕様・推奨は改定されるため、本番投入前に必ず公式で最新値を確認してください。長期キーは『漏れたら終わり』。OIDCの短命トークンに置き換えるのが、いまの正解です。
0. メンタルモデル:なぜ長期キーが「最大の攻撃面」なのか
設計に入る前に、ここだけは腹落ちさせてください。CIに置く長期キーが危険な理由は、抽象論ではなく構造にあります。
0.1 長期キーの何がまずいのか
AWS_SECRET_ACCESS_KEY や GCP のサービスアカウント鍵(JSON)を GitHub Secrets に保存する——これは便利ですが、次の性質を同時に抱えます。
- 無期限:明示的にローテーションしない限り、その鍵は何ヶ月も有効なまま。発行した本人が忘れた頃にも生きている。
- どこからでも使える:鍵は「誰が・どこから使ったか」を区別しません。手元のラップトップからでも、悪意あるフォークのPRからでも、漏洩先の攻撃者のサーバーからでも、同じように本番権限を行使できる。
- 漏洩が静かに進行する:ログへの誤出力、悪意ある依存パッケージ、サードパーティ Action の乗っ取り、
pull_request_targetの設定ミス——CI で秘密が漏れる経路は無数にあります。そして漏れても、鍵はそのまま動き続けるので気づけない。
GitHub 公式が OIDC を勧める理由も、まさにこれです。「クラウドの資格情報を、長期的な GitHub シークレットとして複製する必要がなくなる(no longer need to duplicate your cloud credentials as long-lived GitHub secrets)」と明記しています。**問題は「鍵が漏れること」ではなく、「漏れても被害が止まらない無期限の鍵を、最も狙われる場所(CI)に置くこと」**なのです。
0.2 OIDC federation は何を変えるのか
OIDC(OpenID Connect)federation は、この構造を根本から変えます。流れはこうです。
- ワークフロー実行が始まると、GitHub の OIDC プロバイダ(
token.actions.githubusercontent.com)が、その実行に固有の 署名付きJWT(IDトークン) を発行する。 - このトークンには「どのリポジトリの、どのブランチ/タグ/environment から、どのワークフローが」実行されているかが**クレーム(claim)**として刻まれている。
- ワークフローはこのトークンをクラウド(AWS STS / GCP STS)に提示する。
- クラウド側は、事前に設定された信頼ポリシーと照合し、「
subクレームやその他のクレームが、ロールのOIDC信頼定義に事前設定された条件に一致するか(check if the OIDC token's subject and other claims are a match for the conditions...)」を検証する。 - 一致したときだけ、そのジョブ1回限りで自動失効する短命の一時クレデンシャルを払い出す。
GitHub 公式の表現を借りれば、OIDC トークンは「1つのジョブでのみ有効で、その後自動的に失効する短命のアクセストークン(short-lived access token that is only valid for a single job, and then automatically expires)」と引き換えられます。
鍵は存在しません。 保存すべき秘密が消え、そのうえ「どこから来た実行か」をクラウド側が能動的に検証する。これが鍵レスCI/CDの核心です。
0.3 旧来 vs OIDC:決定表
| 観点 | 長期アクセスキー(旧来) | OIDC 鍵レス(推奨) |
|---|---|---|
| 保存される秘密 | あり(Secretsに永続) | なし(トークンは実行時に発行) |
| 有効期間 | 無期限(手動ローテ) | ジョブ1回限り・自動失効 |
| 発行元の検証 | できない(鍵に主体情報なし) | できる(sub/repo/ref/environment クレーム) |
| 漏洩時の被害 | 気づくまで本番権限を行使され続ける | トークンは即失効・再利用不可 |
| ローテーション運用 | 必要(忘れると事故の温床) | 不要(毎回新規発行) |
| 信頼範囲の絞り込み | 鍵を持つ全員 | repo/branch/environment 単位 |
| 監査 | 鍵単位(誰が使ったか不明瞭) | クレーム単位で追跡可能 |
「ローテーション不要」は地味に見えて巨大です。長期キーの運用で最も多い事故は、ローテーションを忘れて何年も同じ鍵が生き続けることだからです。OIDC はその運用負債そのものを消し去ります(YAGNI:そもそも要らない仕組みは持たない)。
1. すべての出発点:id-token: write パーミッション
AWS でも GCP でも、ワークフロー側の第一歩は共通です。GitHub に OIDC トークンの発行を許可すること。これは permissions ブロックで明示します。
permissions:
id-token: write # GitHub の OIDC プロバイダに JWT を発行させる(必須)
contents: read # actions/checkout がリポジトリを読むため
公式の説明はこうです。id-token: write は「GitHub の OIDC プロバイダが各実行ごとに JSON Web Token を作成することを許可する(allows GitHub's OIDC provider to create a JSON Web Token for every run)」。ただし注意点として、この権限自体はクラウドのリソースを変更する力を持ちません。あくまで「トークンを作っていい」という許可であり、実際に何ができるかはクラウド側のロール/ポリシーが決めます。
最小権限の出発点:
permissionsはジョブ単位でも宣言できます。トークン発行が必要なデプロイジョブにだけid-token: writeを付け、テストだけのジョブには付けない——これだけで攻撃面が狭まります(SRP:ジョブごとに責務と権限を分ける)。リポジトリ全体のデフォルトをpermissions: {}(全拒否)にして、必要なジョブで足していくのが堅牢です。
1.1 sub クレームの形——信頼範囲を決める主役
OIDC トークンに刻まれる最重要クレームが sub(subject)です。GitHub 公式が示す形式は、コンテキストによって変わります。
- ブランチ:
repo:octo-org/octo-repo:ref:refs/heads/main - environment:
repo:octo-org/octo-repo:environment:prod - プルリクエスト:
repo:OWNER/REPO:pull_request - タグ:
repo:OWNER/REPO:ref:refs/tags/v1.0.0
そのほかトークンに含まれる主なクレーム(公式)は次のとおりです。
| クレーム | 例 | 意味 |
|---|---|---|
iss | https://token.actions.githubusercontent.com | 発行者(GitHub の OIDC プロバイダ) |
sub | repo:octo-org/octo-repo:ref:refs/heads/main | 誰が:repo / ブランチ / environment |
aud | (クラウド側で固定。AWSなら sts.amazonaws.com) | 誰宛て:トークンの受け手 |
repository | octo-org/octo-repo | リポジトリ名 |
ref | refs/heads/main | ブランチ/タグ参照 |
environment | prod | environment 名 |
job_workflow_ref | ワークフローファイル参照 | どのワークフロー定義から実行されたか |
クラウド側の信頼設定は、これらのクレームを照合します。特に sub と aud の2つを正しく絞ることが、鍵レス構成の安全性のほぼすべてです。次章から、AWS と GCP それぞれで具体的に見ていきます。
2. AWS:IAM OIDC プロバイダ + ロール信頼ポリシー
AWS 側の鍵レス化は、2つの設定で成立します。
- IAM OIDC アイデンティティプロバイダを作る(GitHub を信頼の起点として AWS に登録)。
- IAM ロールを作り、その**信頼ポリシー(trust policy)**で「どの repo/branch/environment からの OIDC トークンなら
AssumeRoleを許すか」を条件付ける。
2.1 IAM OIDC プロバイダ
GitHub を AWS の信頼できる ID プロバイダとして登録します。公式が示す設定値は次のとおりです。
- プロバイダURL:
https://token.actions.githubusercontent.com - オーディエンス(Audience):
sts.amazonaws.com(公式アクションaws-actions/configure-aws-credentialsを使う場合)
このオーディエンスが、OIDC トークンの aud クレームと一致することを AWS が検証します。「このトークンはSTS宛てに発行されたものか」を確認する仕組みです。
2.2 ロール信頼ポリシー(trust policy)——ここが心臓部
OIDC プロバイダを登録しただけでは、まだ何もできません。「どの GitHub 実行ならこのロールになれるか」を条件で縛るのが信頼ポリシーです。GitHub 公式が示すJSONはこうです。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456123456:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:octo-org/octo-repo:*"
}
}
}
]
}
各要素の意味を分解します。
Action: sts:AssumeRoleWithWebIdentity:Web ID(=OIDCトークン)でロールを引き受ける、という専用アクション。長期キーの世界には存在しない、federation 専用の入口です。Principal.Federated:先ほど登録した OIDC プロバイダの ARN。「GitHub の OIDC を信頼する」という宣言。aud条件(StringEquals):トークンがsts.amazonaws.com宛てであることを厳密一致で要求。これを省くと、別用途のトークンを流用される余地が生まれます。aud は必ずStringEqualsで固定してください。sub条件:ここで「どの repo/branch/environment か」を絞ります。
2.3 sub をどこまで絞るか——ワイルドカードの危険
上の公式例は repo:octo-org/octo-repo:* というワイルドカードを使っています。これは「このリポジトリのあらゆるブランチ・PR・environment から AssumeRole を許す」という意味です。
GitHub 公式は、この点にはっきり警告しています。sub 条件にワイルドカード(*)を使うと「任意のブランチ、プルリクエストのマージブランチ、または environment(any branch, pull request merge branch, or environment)」がそのロールを引き受けられてしまう、と。
これは本番では避けるべきです。たとえば外部コントリビュータが送ってきた PR からも、このロール(=本番権限)が握られる可能性が生まれます。本番デプロイ用ロールなら、StringEquals で1点に固定します。
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:octo-org/octo-repo:ref:refs/heads/main"
}
}
これで「octo-org/octo-repo の main ブランチからの実行だけ」がこのロールになれます。さらに堅くするなら、environment で縛ります。
"token.actions.githubusercontent.com:sub": "repo:octo-org/octo-repo:environment:production"
environment で縛る利点は次節で説明する environment protection rules と組み合わさることです。「production environment に紐づいた実行」かつ「承認者が承認した実行」だけが本番ロールを引ける——という多層防御になります。
絞り込みの原則(ETC:変更が起きる単位を意識する):信頼条件は「この権限を、いつ・どこから使わせたいか」と1対1で対応させます。本番デプロイは
environment:productionに固定、ステージングはref:refs/heads/developに、という具合に用途ごとにロールを分ける。1つのロールに複数用途をStringLikeで詰め込むと、絞り込みが緩くなり、結局ワイルドカードと大差なくなります。
2.4 ワークフロー側:aws-actions/configure-aws-credentials
クラウド側の準備ができたら、ワークフローはこうなります。
name: Deploy to AWS
on:
push:
branches: [main]
permissions:
id-token: write # OIDC トークン発行(必須)
contents: read # checkout 用
jobs:
deploy:
runs-on: ubuntu-latest
environment: production # environment 縛り + 承認ゲートと連動
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
role-session-name: github-actions-deploy
aws-region: ap-northeast-1
# ここから先は一時クレデンシャルで AWS を操作(鍵は一切なし)
- name: Push image to ECR
run: |
aws sts get-caller-identity # 引き受けたロールの確認
# docker build / push, ECS デプロイ など
ポイントは role-to-assume です。保存された鍵ではなく「引き受けたいロールのARN」を渡すだけ。アクションが内部で OIDC トークンを取得し、AssumeRoleWithWebIdentity を呼び、短命クレデンシャルを環境変数に展開してくれます。AWS_ACCESS_KEY_ID も AWS_SECRET_ACCESS_KEY も、どこにも書きません。
Action のピン留め(セキュリティ):公式例が
configure-aws-credentialsを コミットSHA(@e3dd...)で固定しているのは偶然ではありません。サードパーティ Action は乗っ取られ得るため、@v4のような可変タグではなくフルSHAでピン留めするのが、サプライチェーン攻撃への基本防御です。OIDC で鍵を消しても、悪意ある Action に一時クレデンシャルを盗ませては本末転倒です。
3. GCP:Workload Identity Federation(WIF)
GCP 側の鍵レス化は Workload Identity Federation(WIF) で行います。GCP 公式の定義はこうです。WIF は「**サービスアカウント鍵の代わりに、フェデレーションされたID(federated identities instead of a service account key)**を使って」外部ワークロードに GCP リソースへのアクセスを与える仕組みであり、「サービスアカウント鍵に伴う保守とセキュリティの負担を排除する(eliminates the maintenance and security burden associated with service account keys)」。
サービスアカウント鍵(JSON)は、AWS の長期アクセスキーと同じ「漏れたら終わり」の長期秘密です。WIF はこれを短命の OAuth 2.0 トークンに置き換えます。
3.1 登場人物:Pool と Provider
WIF には2つの構成要素があります(公式用語)。
- Workload Identity Pool(ワークロードアイデンティティプール):外部のIDを管理する入れ物。公式は「非Google Cloud環境ごとに1つのプールを推奨」しています。GitHub Actions 用に1つ作る、というイメージです。
- Workload Identity Pool Provider(プロバイダ):GCP とあなたのIDプロバイダ(ここでは GitHub)との関係を記述するもの。GitHub の OIDC issuer をここに設定します。
3.2 Attribute Mapping と Attribute Condition——信頼範囲の絞り込み
WIF の安全性を決めるのが、この2つです。AWS の sub 条件に相当します。
Attribute Mapping(属性マッピング) は、GitHub のトークンのクレームを GCP の属性に変換します。CEL(Common Expression Language)で書きます。公式が示す代表例:
google.subject=assertion.sub(GitHub のsubを GCP の主体IDにマッピング)attribute.repository=assertion.repository(リポジトリ名を独自属性として取り込む)
Attribute Condition(属性条件) は、「どの外部IDなら認証を許すか」を CEL で制限します。公式が強く推奨する設定はこれです。
assertion.repository == 'OWNER/REPO'
公式はこの条件の意義を明確に述べています。これは「他プラットフォーム向けに発行された資格情報が GCP にアクセスするのを防ぎ、confused deputy 問題を緩和する」。
WIF の最重要落とし穴:Attribute Condition を設定しない、あるいは緩く設定すると、GitHub の任意のリポジトリ(=世界中の他人のリポジトリ)から発行されたトークンが、あなたの GCP プロバイダで受理されかねません。GitHub の OIDC issuer は全 GitHub ユーザー共通だからです。プロバイダ作成時に
assertion.repository == 'OWNER/REPO'を必ず付ける——これは AWS のsubをワイルドカードにしない、と完全に同じ問題です。さらにassertion.ref == 'refs/heads/main'を足せばブランチまで縛れます。
3.3 直接アクセス vs サービスアカウント インパーソネーション
WIF には2つのアクセスモデルがあります(公式)。
- Direct resource access(直接リソースアクセス):外部IDに、GCP リソース上のロールを直接付与する。「中間のサービスアカウントも鍵も存在しない(no intermediate service accounts or keys)」最もシンプルな形。
- Service account impersonation(サービスアカウント インパーソネーション):外部IDが中間のサービスアカウントを介してリソースにアクセスする。既存のサービスアカウント権限を再利用したい場合や、一部 GCP サービスがインパーソネーション前提の場合に使います。
「鍵を作らない」という目的はどちらも達成します。違いは中間サービスアカウントを挟むかどうかだけです。新規なら直接アクセスがシンプル(KISS)、既存資産を活かすならインパーソネーション、と選びます。
3.4 ワークフロー側:google-github-actions/auth
ワークフローは公式アクション google-github-actions/auth を使います。GCP 公式は、このアクションが「GitHub の OIDC トークンを Google Cloud のアクセストークンと交換する(exchanges a GitHub OIDC token for a Google Cloud access token)」と説明しています。
name: Deploy to GCP
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # OIDC トークン発行(必須)
steps:
- uses: actions/checkout@v4 # auth の前に必ず checkout
- id: auth
name: Authenticate to Google Cloud (WIF)
uses: google-github-actions/auth@v3
with:
project_id: my-project
# プロジェクト番号 + プール名 + プロバイダ名のフル識別子
workload_identity_provider: projects/123456789/locations/global/workloadIdentityPools/github/providers/my-repo
service_account: deployer@my-project.iam.gserviceaccount.com # インパーソネーション時のみ
# 以降は短命トークンで GCP を操作(鍵JSONは一切なし)
- name: Push to Artifact Registry / deploy Cloud Run
run: gcloud run deploy ...
入力の意味(公式):
workload_identity_provider:「プロジェクト番号・プール名・プロバイダ名を含むフル識別子」。形式はprojects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL/providers/PROVIDER。service_account:インパーソネーションする場合のみ指定するサービスアカウントのメール。直接アクセス(Direct WIF)なら省略します。create_credentials_file:デフォルトtrue。後続のgcloud/ SDK が参照する一時的な認証ファイルを生成。token_format:access_tokenまたはid_token。Cloud Run の IAM 呼び出しなどでid_tokenが要るケースがあります。
公式の重要注意:
google-github-actions/authはactions/checkoutの後に置くこと。順序を逆にすると、認証情報ファイルが checkout で上書き/削除される事故が起きます。
4. AWS と GCP の対応表:頭の中を1枚で整理する
両クラウドを行き来すると混乱しがちなので、設定要素の対応を1枚にまとめます。「鍵を消す」という同じゴールに、各クラウドの語彙でどう到達するかの地図です。
| 役割 | AWS | GCP(WIF) |
|---|---|---|
| GitHub を信頼の起点に登録 | IAM OIDC アイデンティティプロバイダ | Workload Identity Pool Provider |
| GitHub の issuer | token.actions.githubusercontent.com | 同左(GitHub の OIDC issuer) |
| トークンの受け手(aud) | sts.amazonaws.com | プロバイダ設定の audience |
| 「誰からか」を絞る条件 | 信頼ポリシーの sub 条件 | Attribute Condition(assertion.repository == '...') |
| クレーム→内部属性の変換 | (条件で直接参照) | Attribute Mapping(google.subject=assertion.sub) |
| 実権限を与える対象 | IAM ロール | リソースへの直接ロール付与 or サービスアカウント |
| トークン交換の専用API | sts:AssumeRoleWithWebIdentity | GCP Security Token Service |
| ワークフロー側アクション | aws-actions/configure-aws-credentials | google-github-actions/auth |
| ワークフロー側で渡す値 | role-to-assume(ロールARN) | workload_identity_provider(+service_account) |
| 共通の前提 | permissions: id-token: write | permissions: id-token: write |
構造はまったく同じだと分かります。「GitHub を信頼登録 → クレームで発行元を絞る → 短命クレデンシャルを払い出す」。語彙が違うだけで、設計判断は両クラウドで共通(DRY:同じ思考を2回しない)。だからこそ、片方を理解すればもう片方も素早く組めます。
5. Terraform で OIDC プロバイダ / WIF プールを作る
GUI でポチポチ作るのは再現性ゼロで、設定ミス(特に sub / Attribute Condition の絞り込み漏れ)を生みます。OIDC の信頼関係こそ IaC で管理すべきです。誰がいつ何を信頼させたかが、コードレビューと履歴に残るからです。
5.1 AWS:OIDC プロバイダ + ロール
# GitHub Actions を信頼する IAM OIDC プロバイダ
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"] # = aud
# サムプリントは AWS が自動検証するため、近年は固定値ハードコード不要
thumbprint_list = ["ffffffffffffffffffffffffffffffffffffffff"]
}
# main ブランチ + production environment からの実行だけを信頼するロール
data "aws_iam_policy_document" "trust" {
statement {
effect = "Allow"
actions = ["sts:AssumeRoleWithWebIdentity"]
principals {
type = "Federated"
identifiers = [aws_iam_openid_connect_provider.github.arn]
}
# aud は厳密一致で固定(流用防止)
condition {
test = "StringEquals"
variable = "token.actions.githubusercontent.com:aud"
values = ["sts.amazonaws.com"]
}
# sub も厳密一致で1点固定(ワイルドカード禁止)
condition {
test = "StringEquals"
variable = "token.actions.githubusercontent.com:sub"
values = ["repo:octo-org/octo-repo:environment:production"]
}
}
}
resource "aws_iam_role" "github_deploy" {
name = "github-actions-deploy"
assume_role_policy = data.aws_iam_policy_document.trust.json
}
# 最小権限:このロールが触れてよいリソースだけを許可(例は雛形)
resource "aws_iam_role_policy" "deploy_least_privilege" {
role = aws_iam_role.github_deploy.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = ["ecr:GetAuthorizationToken", "ecr:BatchGetImage", "ecr:PutImage"]
Resource = "*" # 本番では特定 ECR リポジトリ ARN に絞る
}]
})
}
condition を StringEquals で書いている点に注目してください。ワイルドカードを使う StringLike ではなく、1点固定です。コードに残るので「なぜここを固定したか」がレビューで議論でき、緩める変更には必ず承認が要る——これが IaC で信頼関係を扱う最大の利点です。
5.2 GCP:WIF プール + プロバイダ + 属性条件
# GitHub Actions 用の Workload Identity Pool
resource "google_iam_workload_identity_pool" "github" {
workload_identity_pool_id = "github"
display_name = "GitHub Actions"
}
# GitHub を OIDC プロバイダとして登録
resource "google_iam_workload_identity_pool_provider" "github" {
workload_identity_pool_id = google_iam_workload_identity_pool.github.workload_identity_pool_id
workload_identity_pool_provider_id = "my-repo"
oidc {
issuer_uri = "https://token.actions.githubusercontent.com"
}
# Attribute Mapping:GitHub のクレームを GCP 属性へ
attribute_mapping = {
"google.subject" = "assertion.sub"
"attribute.repository" = "assertion.repository"
"attribute.ref" = "assertion.ref"
}
# Attribute Condition:このリポジトリ以外のトークンを門前払い(必須)
attribute_condition = "assertion.repository == 'octo-org/octo-repo'"
}
attribute_condition は必ず設定してください。これが無いと、前述のとおり世界中の任意の GitHub リポジトリのトークンがプロバイダに受理され得ます。assertion.repository == 'octo-org/octo-repo' で発行元リポジトリを固定し、必要なら && assertion.ref == 'refs/heads/main' でブランチまで縛ります。
HCL の注意:
oidc/attribute_mappingのフィールド名やgoogle_iam_workload_identity_pool_providerのスキーマは Provider バージョンで差異が出ることがあります。上記は構造を示す雛形です。本番適用前に、お使いの Google Provider バージョンのリソース仕様を確認してください(要確認)。
6. 多層防御:信頼範囲をさらに固くする実務
OIDC を入れただけで満足しないでください。鍵レス化は「土台」であって、その上に信頼範囲を絞る運用を積み上げてはじめて堅牢になります。実案件で効いた打ち手を挙げます。
6.1 environment protection rules と組み合わせる
sub を environment:production で縛り、かつ GitHub の environment protection rules(必須レビュアー、待機タイマー、デプロイ可能ブランチ制限)を設定します。すると本番ロールを引けるのは——
productionenvironment に紐づいた実行で、- かつ承認者が承認した実行だけ、
という二重ゲートになります。OIDC の sub 検証(クラウド側)と、environment 承認(GitHub 側)が直交する防御層として働きます。
6.2 PR からの濫用を断つ
最も多い事故が「フォークやPRからの実行で本番権限が握られる」ケースです。対策は明快です。
- 本番ロールの
subをref:refs/heads/mainまたはenvironment:productionに固定し、pull_request由来のトークン(subがrepo:OWNER/REPO:pull_request)を最初から弾く。 - ワイルドカード
repo:OWNER/REPO:*は、まさにこのpull_requestを含めてしまうので本番ロールには絶対に使わない。 - 検証用のジョブには、本番とは別の読み取り専用ロールを割り当て、書き込み権限を渡さない。
GitHub 公式が「ワイルドカードは任意のブランチ・PRマージブランチ・environment を許す」と警告するのは、この PR 濫用を念頭に置いています。
6.3 ロールは「用途 × 環境」で分割する
1つの全能ロールを共有するのは、長期キーを共有するのと同じ過ちです。用途と環境ごとにロールを分け、各ロールには最小権限だけを与えます。
| ジョブ | 信頼する sub / 条件 | 付与する権限(最小) |
|---|---|---|
| 本番デプロイ | environment:production | ECR push + 該当 ECS サービス更新のみ |
| ステージングデプロイ | ref:refs/heads/develop | ステージング ECR / ECS のみ |
| PRでの検証(read-only) | pull_request | 読み取り専用(plan / lint のみ) |
これは私が AWS 側で実際に採っている分割です。Terraform は plan(PRで tfsec による静的解析)/ apply(権限境界付きロール)/ drift検知(定期 cron → 差分を Issue 起票) をワークフローで分離し、それぞれに別の OIDC ロールを割り当てています。**「PR では絶対に apply できない」**という構造が、最小権限を保証します(SRP:plan と apply は別の責務 = 別ロール)。
6.4 aud は絶対に省かない
AWS の aud 条件、GCP のプロバイダ audience は、トークンの宛先を保証します。これを緩めると、別用途に発行されたトークンの流用余地が生まれます。aud は常に StringEquals で固定——例外なし、と覚えてください。
7. 実例:両クラウドで鍵レスCI/CDを運用する
抽象論で終わらせないために、私が実際に運用している2つの構成を示します。AWS と GCP の両方で、長期キーは1本も存在しません。
7.1 GCP 側:放送事業者の社内AIプラットフォーム
国内大手放送事業者向けの社内AIプラットフォーム(事例)では、CI/CD を Workload Identity Federation(OIDC)で鍵レスにしています。
- GitHub Actions からサービスアカウント鍵(JSON)を一切発行せずに GCP へ認証。漏れたら終わりの鍵が、リポジトリにもCIにも存在しない。
- Cloud Build で stg / prod を出し分け、デプロイ先を環境ごとに分離。
- DBマイグレーションは専用ジョブに分離し、アプリデプロイと責務を分けた(SRP)。マイグレーションの失敗がデプロイ全体を巻き込まない。
- CodeQL・依存更新も自動化し、サプライチェーン側の防御も常時稼働。
- インフラは100% Terraform(約71モジュール)。WIF プール/プロバイダ/属性条件も当然 IaC 管理で、信頼関係の変更が必ずレビューに乗る。
7.2 AWS 側:木材業界DXのデプロイパイプライン
別案件(木材業界DX)では AWS 側を OIDC で組んでいます。
- GitHub Actions OIDC(長期キーなし)で ECR → ECS 強制デプロイ。
AWS_SECRET_ACCESS_KEYを Secrets に置かず、ロール ARN を渡すだけ。 - S3同期 + CloudFront 無効化もOIDCロール経由。
- Terraform は plan(PRで
tfsec)/ apply(権限境界付きロール)/ drift検知(定期 cron → Issue 起票) を分離し、各段階に別ロールを割り当て。
この2案件を通じて言えるのは、OIDC 鍵レス化はクラウドを問わず再現できる定石だということです。AWS の sub 条件と GCP の Attribute Condition は語彙こそ違え、やっていることは同じ——「発行元を絞って、短命トークンを払い出す」。一度身につければ、どちらのクラウドでも数十分で組めます。
8. まとめ:鍵レスCI/CD チートシート
最後に、迷ったときの早見表です。
- 大原則:CIに長期キーを置かない。
AWS_SECRET_ACCESS_KEYも サービスアカウント鍵JSON も Secrets から消す。 - 共通の第一歩:ワークフローに
permissions: id-token: write(+ checkout 用contents: read)。ジョブ単位で最小化する。 - AWS:IAM OIDC プロバイダ(
token.actions.githubusercontent.com/ audsts.amazonaws.com)→ ロール信頼ポリシーでaudをStringEquals固定、subをrepo:OWNER/REPO:environment:productionなどに1点固定 → ワークフローはaws-actions/configure-aws-credentialsにrole-to-assumeを渡すだけ。 - GCP:Workload Identity Pool + Provider → Attribute Condition で
assertion.repository == 'OWNER/REPO'を必ず設定 → ワークフローはgoogle-github-actions/authにworkload_identity_provider(+必要ならservice_account)を渡す。actions/checkoutの後に置く。 - ワイルドカード厳禁:
subの*も、Attribute Condition の未設定も、「任意のブランチ・PR・他人のリポジトリ」を招き入れる。本番ロールは常に1点固定。 - 多層防御:environment protection rules で承認ゲート、用途×環境でロール分割、PR には read-only ロール、Action はSHAピン留め。
- IaC で管理:OIDC プロバイダ/WIF プール/信頼条件は Terraform に。信頼関係の変更を必ずレビューに乗せる。
長期キーは「漏れたら終わり」の静かな時限爆弾です。OIDC federation は、その爆弾そのものを撤去し、代わりに「1回限りで失効する、発行元が検証可能な短命トークン」に置き換えます。鍵レス化は、もはや上級者の贅沢ではなく、CI/CD の標準装備です。
私は、生成AI(Claude Code)を相棒に一人で開発を回すスタイルで、AWS と GCP の両方で鍵レスCI/CDを本番運用しています。Terraform で OIDC の信頼関係を IaC 化し、最小権限ロールと environment 承認ゲートまで含めて設計・実装・運用を一気通貫で担えます。「うちの GitHub Actions、まだ長期キー貼ってるんだけど…」——その置き換えこそ、最も費用対効果の高いセキュリティ投資です。要件整理の段階からでも、お気軽にご相談ください。
参考(公式ドキュメント)
- About security hardening with OpenID Connect(GitHub Docs) — OIDC の概念・
id-token: write・subクレーム形式・短命トークン - Configuring OpenID Connect in Amazon Web Services(GitHub Docs) — IAM OIDC プロバイダ・信頼ポリシー(
aud/sub)・configure-aws-credentials・ワイルドカード警告 - Configuring OpenID Connect in Google Cloud Platform(GitHub Docs) —
google-github-actions/auth・workload_identity_provider/service_account - Workload Identity Federation(Google Cloud Docs) — WIF の概念・プール/プロバイダ・Attribute Mapping/Condition・鍵を作らない直接アクセス
- google-github-actions/auth(GitHub) — Direct WIF / インパーソネーション・各入力(
workload_identity_providerほか)・checkout 順序