アクセスキー不要で GitHub Actions から AWS へ: OIDC の仕組みと最小テスト
INDEX
GitHub Actions から AWS を操作したいとき、IAM ユーザーのアクセスキーを GitHub Secrets に入れていませんか。 OIDC を使うと、長期のキーを一切持たずに、実行のたびに短命の認証情報を受け取れます。
この記事では、次の順で整理します。
- OIDC とは何か
- GitHub Actions と AWS はどうつながるのか
- 自分の環境で試せる、最小限のテスト(所要時間は 15 分ほど)
- OIDC が使われるほかの場面
OIDC とは#
OIDC(OpenID Connect) は、「ある主体が誰なのか」を、信頼できる第三者が 署名付きの身分証 で証明するための仕組みです。 身近な例は「Google でログイン」です。サイト側はあなたのパスワードを知らなくても、Google が「この人は確かに本人です」と署名付きで保証してくれるので、ログインを許可できます。
ここで出てくる言葉は 4 つだけ覚えれば十分です。
| 言葉 | 意味 | 「Google でログイン」の場合 |
|---|---|---|
| IdP(身分を証明する側) | 身分証を発行する | |
| 利用する側 | IdP を信頼して、身分証を受け取る | ログインしたいサイト |
| トークン(JWT) | 署名付きの身分証。「誰が」「どこから」などの情報が入っている | ログイン用のトークン |
| クレーム | トークンに入っている個々の情報 | メールアドレス、名前など |
GitHub Actions の OIDC は、これを 人ではなくワークフロー(ジョブ) に当てはめたものです。 GitHub が IdP になり、「このジョブは、このリポジトリの、このブランチで動いています」という身分証をジョブごとに発行します。
GitHub Actions と AWS の関係#
役割を整理すると、次のようになります。
| 登場人物 | 役割 |
|---|---|
| Actions ランナー | ワークフローを実行する。id-token: write 権限があると、身分証(OIDC トークン)を要求できる |
| GitHub の OIDC プロバイダ | IdP。リポジトリやブランチの情報を載せた JWT に署名して発行する |
| AWS の IAM OIDC プロバイダ | 「GitHub が出した JWT を信頼する」という、AWS 側の事前登録 |
| IAM ロール(信頼ポリシー) | JWT の中身が条件に合うときだけ、そのロールを引き受けさせる |
| AWS のリソース | S3 や CloudFront など、実際に操作したいもの |
大事なのは、事前に「GitHub を信頼する」と AWS に登録しておく 点です。この信頼関係があるから、パスワードもアクセスキーも渡さずに済みます。
認証の流れ#
- ランナーが GitHub に OIDC トークンを要求し、署名付きの JWT を受け取る(①②)
- JWT を添えて AWS STS の
AssumeRoleWithWebIdentityを呼ぶ(③) - STS が GitHub の公開鍵で署名を検証し、ロールの信頼ポリシーの条件と照合する(④)
- 条件を満たしたときだけ、一時的な認証情報が返る(⑤)
- 以降の
awsコマンドは、その認証情報で動く(⑥)
JWT には、たとえば次のようなクレームが入っています。
{
"iss": "https://token.actions.githubusercontent.com",
"aud": "sts.amazonaws.com",
"sub": "repo:<owner>/<repo>:ref:refs/heads/main",
"repository": "<owner>/<repo>",
"ref": "refs/heads/main",
"event_name": "push"
}
信頼ポリシーで一番大事なのが sub です。aud だけを見ていると、GitHub 上の どのリポジトリからでも このロールを引き受けられてしまいます。必ず sub でリポジトリを絞ります。
アクセスキー方式との違い#
| アクセスキー方式 | OIDC 方式 | |
|---|---|---|
| GitHub に保存するもの | アクセスキーとシークレット | ロールの ARN だけ(秘密情報ではない) |
| 有効期間 | 削除するまで無期限 | 実行ごとに発行、約 1 時間で失効 |
| ローテーション | 自分で回す | 不要 |
| 利用元の絞り込み | 難しい | リポジトリ・ブランチ・イベントで絞れる |
最小限のテストで試す#
ここからは手を動かします。ゴールは「GitHub Actions が、アクセスキー無しで、自分の AWS アカウントの ID を表示する」ことです。 S3 などは使わず、必要なものを最小にしています。
用意するもの#
- AWS アカウントと、IAM を操作できる権限(試すだけなら管理者権限で十分です)
- AWS CLI(
aws sts get-caller-identityが通る状態) - GitHub のリポジトリ(空のものや使い捨てのもので構いません)
以降のコマンドで使う値を、先にシェル変数に入れておきます。自分の値に書き換えてください。
export GH_OWNER="your-github-name" # GitHub のユーザー名 or Organization 名
export GH_REPO="your-repo" # リポジトリ名
export ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
手順 1: OIDC プロバイダを登録する(アカウントに 1 回だけ)#
AWS に「GitHub を信頼する」と登録します。
aws iam create-open-id-connect-provider \
--url https://token.actions.githubusercontent.com \
--client-id-list sts.amazonaws.com
同じ URL のプロバイダは 1 アカウントに 1 つしか作れません。EntityAlreadyExists と出たら、すでにあるのでそのまま次へ進みます。
以前は証明書のサムプリントの指定が必要でしたが、今の GitHub のプロバイダでは不要です。
手順 2: IAM ロールを作る#
信頼ポリシー(誰が引き受けられるか)を作り、ロールを作成します。
cat > trust.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::${ACCOUNT_ID}:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:${GH_OWNER}/${GH_REPO}:ref:refs/heads/main"
}
}
}
]
}
EOF
aws iam create-role \
--role-name gha-oidc-test \
--assume-role-policy-document file://trust.json \
--query Role.Arn --output text
最後に表示される ARN(arn:aws:iam::123456789012:role/gha-oidc-test のような文字列)を控えておきます。
ポイントは次の 3 つです。
Principalは Federated。手順 1 で登録した OIDC プロバイダを指すActionはsts:AssumeRoleWithWebIdentity(普通のsts:AssumeRoleではない)subでmainブランチ に限定している
今回のテストでは、ロールに権限ポリシーを付けません。sts get-caller-identity は権限が無くても実行できるからです。
実際に S3 などを操作するときは、必要な権限だけを付けます。
手順 3: ワークフローを置く#
リポジトリに .github/workflows/oidc-test.yml を作ります。
name: oidc-test
on:
workflow_dispatch: # 手動実行
push:
branches: [main]
permissions:
id-token: write # OIDC トークンを要求するために必要
contents: read
jobs:
whoami:
runs-on: ubuntu-latest
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/gha-oidc-test # 控えた ARN に置き換える
aws-region: ap-northeast-1
- run: aws sts get-caller-identity
permissions の id-token: write が無いとトークンを取得できず失敗します。
permissions を書くと、書かなかった権限は付かなくなるので、contents: read も一緒に書いておきます。
手順 4: 動かして確認する#
main に push するか、GitHub の Actions タブから oidc-test を手動実行します。
成功すると、ログに次のような出力が出ます。
{
"UserId": "AROAXXXXXXXXXXXXXXXXX:GitHubActions",
"Account": "123456789012",
"Arn": "arn:aws:sts::123456789012:assumed-role/gha-oidc-test/GitHubActions"
}
assumed-role/gha-oidc-test と出ていれば成功です。リポジトリにはアクセスキーを何も保存していません。
おまけ: 絞り込みが効いているか確かめる
main 以外のブランチで同じワークフローを動かしてみてください。sub の条件に合わないので、次のエラーで失敗します。これが「絞れている」状態です。
Not authorized to perform sts:AssumeRoleWithWebIdentity
後片付け#
aws iam delete-role --role-name gha-oidc-test
rm trust.json
OIDC プロバイダ(手順 1)は、他の用途と共有されることが多いので、残して構いません。
実運用にするときのポイント#
- 権限は用途ごとにロールを分けて、最小にする。 頻繁に動くデプロイ用は特に小さくする
subはできるだけ具体的に書く。 次のように、トリガーごとに値が変わる
| トリガー | sub の例 |
|---|---|
| ブランチへの push | repo:<owner>/<repo>:ref:refs/heads/main |
| pull request | repo:<owner>/<repo>:pull_request |
| タグ | repo:<owner>/<repo>:ref:refs/tags/v1.0.0 |
| Environment を使うジョブ | repo:<owner>/<repo>:environment:production |
- 複数の条件を許可したいとき は、
StringLikeでsubを配列にする
"StringLike": {
"token.actions.githubusercontent.com:sub": [
"repo:<owner>/<repo>:ref:refs/heads/main",
"repo:<owner>/<repo>:pull_request"
]
}
- ロールの ARN は Secrets でなくてもよい。 秘密情報ではないので、Variables に置いても構いません
つまずきやすい点#
Not authorized to perform sts:AssumeRoleWithWebIdentity
最もよく見るエラーで、原因はほぼ信頼ポリシーの条件の不一致です。
subのブランチ名やイベントが実際と違う(pull request なのにref:refs/heads/mainだけを許可している、など)- リポジトリ名の綴りや大文字小文字が違う
audがsts.amazonaws.comになっていない
Credentials could not be loaded / トークンが取れない
ワークフローかジョブの permissions に id-token: write がありません。
sub に数値の ID が入っている
リポジトリによっては、sub が名前ではなく ID の形式(repo:<owner>@<owner-id>/<repo>@<repo-id>:...)になります。名前を書いても通らないときは、次のコマンドで接頭辞を確かめます。
gh api repos/<owner>/<repo>/actions/oidc/customization/sub
fork からの pull request ではトークンが出ない
fork 元の PR では、既定で id-token: write が使えません。外部の人の PR から AWS に入れない、安全側の仕様です。
OIDC のほかの使い道#
「署名付きの身分証を見せて、短命の権限をもらう」という形は、GitHub Actions と AWS の組み合わせに限りません。 同じ考え方が、次のような場面で使われています。
| 場面 | 内容 |
|---|---|
| GitHub Actions から他のクラウドへ | Google Cloud の Workload Identity Federation や、Azure の federated credentials でも、同じ形でキーなしの認証ができる |
| 他の CI/CD サービス | GitLab CI/CD、CircleCI、Bitbucket Pipelines なども OIDC トークンを発行でき、AWS などと連携できる |
| Kubernetes | EKS では、Pod に紐づくサービスアカウントのトークンで IAM ロールを引き受ける(IRSA)。仕組みは OIDC と同じ |
| パッケージの公開 | PyPI や npm などの Trusted Publishing では、API トークンの代わりに、CI の身分証で公開を許可する |
| ログイン(SSO) | 「Google でログイン」のように、IdP が本人確認をして、サービスにログインさせる。Amazon Cognito や Microsoft Entra ID も同様 |
どれも、長期の秘密情報を配らず、相手の身元を署名で確かめる ことで、漏えいの危険を減らす仕組みです。
まとめ#
- OIDC は、信頼できる IdP が署名付きの身分証(JWT)で相手を証明する仕組み
- GitHub Actions では GitHub が IdP となり、AWS は事前に「GitHub を信頼する」と登録しておく
- 保存するのはロールの ARN だけで、長期のアクセスキーは不要になる
- 安全性は 信頼ポリシーの
sub条件 で決まる。audだけで済ませない - 同じ考え方は、ほかのクラウド、CI/CD、Kubernetes、パッケージ公開、ログインでも使われている