GitHub Actions から AWS を操作したいとき、IAM ユーザーのアクセスキーを GitHub Secrets に入れていませんか。 OIDC を使うと、長期のキーを一切持たずに、実行のたびに短命の認証情報を受け取れます。

この記事では、次の順で整理します。

  1. OIDC とは何か
  2. GitHub Actions と AWS はどうつながるのか
  3. 自分の環境で試せる、最小限のテスト(所要時間は 15 分ほど)
  4. OIDC が使われるほかの場面

OIDC とは#

OIDC(OpenID Connect) は、「ある主体が誰なのか」を、信頼できる第三者が 署名付きの身分証 で証明するための仕組みです。 身近な例は「Google でログイン」です。サイト側はあなたのパスワードを知らなくても、Google が「この人は確かに本人です」と署名付きで保証してくれるので、ログインを許可できます。

ここで出てくる言葉は 4 つだけ覚えれば十分です。

言葉意味「Google でログイン」の場合
IdP(身分を証明する側)身分証を発行するGoogle
利用する側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 に登録しておく 点です。この信頼関係があるから、パスワードもアクセスキーも渡さずに済みます。

Actions ランナーGitHub OIDCAWS STS / IAMAWS のリソース① トークンを要求② 署名付き JWT③ AssumeRoleWithWebIdentity + JWT④ 署名と条件を検証⑤ 一時的な認証情報⑥ aws コマンドで操作

認証の流れ#

  1. ランナーが GitHub に OIDC トークンを要求し、署名付きの JWT を受け取る(①②)
  2. JWT を添えて AWS STS の AssumeRoleWithWebIdentity を呼ぶ(③)
  3. STS が GitHub の公開鍵で署名を検証し、ロールの信頼ポリシーの条件と照合する(④)
  4. 条件を満たしたときだけ、一時的な認証情報が返る(⑤)
  5. 以降の 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 の例
ブランチへの pushrepo:<owner>/<repo>:ref:refs/heads/main
pull requestrepo:<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 などと連携できる
KubernetesEKS では、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、パッケージ公開、ログインでも使われている