本文へスキップ
webkida wiki
戻る

【GitHub Actions】OIDCでクラウドに認証する

GitHub ActionsでAWSなどへデプロイするとき、シークレットをどう渡すかで迷う人は多いです。

IAMユーザーのAccess KeyをGitHub Secretsに登録する。workflowからそのシークレットを読み出して使う、という流れが定番です。

一方で、GitHub ActionsはOIDC(OpenID Connect)に対応しており、job実行のたびに短命なJWTを発行します。AWS が JWT を検証したうえで短命なシークレットを渡す方式も取れます。長期シークレットをリポジトリに預けずに済むので、デプロイのセキュリティを上げたいときに検討する価値があります。

以下では、用語、従来方式の弱点、OIDCのメリット、認証フロー、必要な設定を順に整理します。全体像は GitHub 公式の解説 も参考になります。

用語

GitHub OIDC

GitHub Actionsのjobが走ると、GitHubが短命なJWT(JSON Web Token)を発行します。GitHubが認証基盤(Identity Provider)の役割を担い、job実行のたびに「今このjobが動いている」という情報をJWTに載せて渡します。

長期のAccess KeyをSecretsに置く必要はなく、jobのたびに新しいJWTが使われます。

Trust Policy

AWS IAMロールの設定の一つで、「誰がこのロールを引き受け(AssumeRole)られるか」を決めます。GitHub OIDC連携では、Trust PolicyがJWTの内容(どのリポジトリから、どのブランチで、など)をチェックするゲートになります。

条件を満たした JWT だけがロールを引き受けられ、短命なシークレットが渡されます。

Permission Policy

AWS IAMロールのもう一つの設定で、「ロールを引き受けたあと、どの AWS 操作が許されるか」を決めます。Trust Policy が入口(AssumeRole)を制御するのに対し、Permission Policy は引き受け後の S3 や IAM など API 呼び出しの範囲を決めます。

Trust Policy と混同しやすいですが、別の設定です。本番ロールに S3 書き込み、プレビューロールに読み取りだけ、といった AWS 操作の権限 は Permission Policy で分けます。

どちらも最終的には AWS のシークレット

GitHub Actions から AWS API を呼ぶとき、実際にリクエストへ載せるのは AWS から発行されたシークレット(Access Key ID と Secret Access Key のペア)です。

長期シークレット方式では、IAM ユーザー作成時に得たシークレットを GitHub Secrets に置き、workflow がそのまま使います。OIDC 方式では、GitHub が JWT を発行し、AWS が Trust Policy で JWT を検証したうえで、短命なシークレットを新たに発行します。

API を呼ぶ段階で AWS がやっているのは、シークレットの検証だけです。長期か短命かは意識せず、有効なシークレットかどうかを見ています。方式の違いは、シークレットをどこに置き、どれくらいの寿命で発行するかにあります。

JWT は API 呼び出し用のシークレットではなく、AWS に身元を示して短命なシークレットをもらうための証明です。

従来のやり方:長期シークレットを GitHub に登録する

IAMユーザーのAccess Keyを発行し、GitHub Secretsに登録してworkflowから使う、という流れがよくあります。

env:
  AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
  AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

手軽に動かせますが、一度 Secrets に置いたシークレットは、無効化するまでずっと使えます

ざっくりした流れ

flowchart TB
  subgraph pre ["① 事前準備(1回だけ)"]
    direction LR
    iam["IAM ユーザー"] --> key["Access Key を発行"]
  end

  secrets[("GitHub Secrets<br/>長期シークレットを常時保管")]

  subgraph run ["② job 実行時"]
    direction LR
    wf["GitHub Actions workflow"] --> aws["AWS API"]
  end

  subgraph issue ["③ 問題になりやすい点"]
    direction LR
    local["ローカル PC 等"] --> reuse["同じ Access Key で<br/>workflow 外からも API 呼び出し"]
  end

  key --> secrets
  secrets -->|"毎回読み出す"| wf
  secrets -.->|"シークレットをコピーして使い回し"| local
  reuse -.-> aws

  pre --> secrets

図: Access Key は Secrets に常時保管される。workflow 外のローカル等からも同じシークレットが使える。

同じAccess Keyを、workflow以外(ローカル開発や別ツール)でも使い回せてしまう点が、運用上の盲点になりやすいです。

弱点

観点起きやすいこと
有効期限無効化・ローテーションするまで永続
ローテーションAWS・Secrets・他環境を手動で揃える必要がある
権限設計1つのシークレットを本番・PR・複数リポジトリで使い回しがち
監査CloudTrailはIAMユーザー単位。GitHubのjobとは別系統

長期シークレットそのもののリスク

Access Keyは有効な間、何度でもAPIを呼べます。漏えいしても無効化するまで被害が続く、というリスクがあります。

キーローテーションの面倒さ

推奨される定期ローテーションは、AWS・GitHub Secrets・ローカル環境など複数箇所を揃える作業になります。更新漏れがどこか一つでも残りやすいです。

権限設計が粗くなりがち

デプロイ用IAMユーザー1つに広い権限を付け、本番とPRプレビューで同じシークレットを使う、といった形になりがちです。用途ごとに必要最小限へ絞りにくくなります。

AWS側のログだけでは、どのjobから来た操作かわからない

CloudTrailにはIAMユーザーとしてのAPI呼び出しは残りますが、認証主体は常にそのAccess Keyです。GitHub Actionsのどのworkflow・jobか、ローカルからの操作かは、AWS側のログだけでは区別できません。

比較:長期シークレット vs OIDC

観点長期シークレット(Secrets)OIDC
GitHubに置くものAccess Key(長期)AWS シークレットは不要(ロールARNのみ)
有効期限無効化まで永続job単位で自動失効
ローテーション手動(AWS + Secrets + 他)短命シークレットは自動発行
権限の切り方IAMユーザー単位repository / ref / environment で分岐
監査ログIAMユーザーとして記録ロールセッション + claim / tags

OIDCを使うメリット

前節の弱点に対して、OIDC では次のように変わります。

ざっくりした流れ

flowchart TB
  subgraph pre ["① 事前準備(1回だけ)"]
    direction LR
    oidc["OIDC プロバイダ<br/>(AWS に登録)"]
    role["IAM ロール<br/>+ Trust Policy"]
    oidc --- role
  end

  subgraph run ["② job 実行時"]
    direction TB
    wf["GitHub Actions workflow"]
    idp["GitHub OIDC<br/>(JWT 発行)"]
    gate{{"Trust Policy<br/>claim を検証"}}
    aws["AWS API"]

    wf -->|"JWT を要求"| idp
    idp -->|"短命 JWT"| wf
    wf -->|"ロールを引き受け"| gate
    gate -->|"短命シークレット"| wf
    wf -->|"API 呼び出し"| aws
  end

  subgraph diff ["③ 長期シークレット方式との違い"]
    direction LR
    nokey["Secrets に<br/>AWS シークレットは不要"]
    temp["job 終了で<br/>シークレットは失効"]
  end

  pre --> run
  role -.->|"検証ルール"| gate
  run --> diff

図: job ごとに JWT を発行し、Trust Policy 通過後にだけ短命シークレットが渡る。

GitHubに長期シークレットを置かず、job実行のたびに短命なシークレットだけをもらうのが、長期シークレット方式との大きな違いです。

従来の弱点OIDCでの変化
長期シークレットのリスクSecretsにAccess Keyを置かない
ローテーションの手間短命シークレットはjobごとに自動発行・失効
権限設計の粗さTrust Policyでrepo / ref / environmentを条件にできる
監査の紐づけCloudTrailにロールセッション + claimを載せやすい

長期シークレットをGitHubに置かなくてよい

AWS側でGitHubを信頼するOIDCプロバイダを設定し、workflowは実行のたびに短命なJWTを取得してロールを引き受けます。漏えいの対象になる「リポジトリに眠っている長期シークレット」がなくなります。

キーローテーションの手間が減る

GitHub側で AWS シークレットを回し続ける必要はありません。メンテの中心は、Trust PolicyとIAMロールの設計に移ります。

リポジトリ・ブランチなどで権限を切れる

IAMロールに付ける Permission Policy だけでは、「このリポジトリの main ブランチから来た job だけ」といった条件は書きにくいです。OIDC では JWT に repositoryref が自動で入るので、Trust Policy で repo / branch を条件にできます。

本番用と PR プレビュー用でロールを分け、Trust Policy で main 専用・PR 専用と入口を切り分けられます。repo / branch の条件は Trust Policy に書き、workflow は引き受けるロール ARN を指定するだけです。

AWS側にも、GitHub Actions由来の文脈を載せやすい

AssumeRoleWithWebIdentityで引き受けたセッションはCloudTrailに残り、claimをsession nameやsession tagsに載せる設定も取れます。どのリポジトリ・ブランチ・workflowからの操作かを、AWS側のログにも残しやすくなります。

OIDCの認証フロー

OIDC の流れは 事前準備(1回)job 実行時(毎回) の2段階に分かれます。

【事前準備】AWS 側(Trust Policy)

「GitHub が発行した JWT で、my-org/my-repomain ブランチから来たやつだったら、この IAM ロールを貸していいよ」

まだ job は動いておらず、身元の条件だけをあらかじめ決めておく段階です。

【job 実行時】GitHub Actions → AWS

workflow:「JWT 持ってきました。my-org/my-repomain から来た job です。ロールを引き受けさせてください」

GitHub OIDC が job ごとに JWT(身分証)を発行し、workflow がそれを AWS に見せに行きます。

【job 実行時】AWS 側

AWS:「JWT の sub を見るね…… Trust Policy の Condition と一致してる。OK、短命シークレットを渡すよ

AWS は JWT の中身(subrepository, ref など)と、事前準備で書いた Condition を照合します。一致すれば短命なシークレットが返り、S3 デプロイなどが実行できます。不一致の場合はここで拒否されます。

JWT(GitHub 発行)と短命シークレット(AWS 発行)は別物です。前者は「こういう job です」の申告、後者が AWS API を呼ぶためのシークレットです。

事前準備

workflow が動く前に、AWS 側と GitHub 側で信頼関係とロールを整えます。やることは次の表のとおりです。

どこ何をする
AWSGitHub の OIDC プロバイダ(token.actions.githubusercontent.com)を登録する
AWSデプロイ用 IAM ロールを作り、Trust Policy で「どの repo / branch から来た JWT なら OK か」を書く
AWS同じロールに Permission Policy を付け、S3 書き込みなど 引き受けたあと何ができるか を決める
GitHubworkflow に permissions: id-token: write を付け、JWT を取得できるようにする
GitHubconfigure-aws-credentials 等で 引き受けるロール ARN を指定する

この段階では API は呼ばれません。GitHub が AWS を信頼する、AWS が GitHub 発行の JWT を信頼する、という 土台を作る作業 です。

job 実行時

workflow の job が走るたびに、次の流れで JWT が発行され、短命シークレットに交換されます。

sequenceDiagram
    participant GH as GitHub Actions
    participant IdP as GitHub OIDC Provider
    participant AWS as AWS (IAM)

    GH->>IdP: ① JWT を要求
    IdP-->>GH: ② JWT を返す(sub, ref, repository など)
    GH->>AWS: ③ JWT を提示して AssumeRoleWithWebIdentity
    AWS->>AWS: ④ Trust Policy で claim を検証
    AWS-->>GH: ⑤ 短命シークレットを返す
    GH->>AWS: ⑥ 短命シークレットで S3 デプロイ等を実行
段階誰が何が起きる
① JWT 要求workflow → GitHub OIDCjob の文脈(repo, branch, workflow 等)を載せた JWT を発行してもらう
② JWT 受け取りGitHub OIDC → workflowJWT(sub, ref, repository, workflow など)が job に渡る
③ ロール引き受けworkflow → AWSJWT と引き受ける IAM ロール ARN を AWS に送る
④ 検証AWS IAMTrust Policy と JWT の claim を照合。条件不一致ならここで拒否(シークレットはまだ返らない)
⑤ シークレット発行AWS → workflow短命な Access Key / Session Token が job に返る
⑥ API 実行workflow → AWS短命シークレットでデプロイ等を実行。job 終了後は失効

②の JWT(GitHub 発行)と ⑤ の短命シークレット(AWS 発行)は別物です。JWT は身元証明、短命シークレットが AWS API を呼ぶための資格情報です。

長期シークレット方式との違いは、⑥ の前に毎回 ①〜⑤ が挟まる 点です。Secrets からシークレットを読むのではなく、job ごとに JWT → 短命シークレット、という経路になります。

必要な設定

どこに何を書くかは、次の3つに分かれます。

設定どこ何を決める
Trust PolicyAWS(IAMロール)どの repo / branch / environment から来た JWT なら、このロールを引き受けていいか
workflowGitHub(.github/workflows/*.ymlどの job が、どの IAM ロールを引き受けるか
Permission PolicyAWS(IAMロール)ロールを引き受けたあと、S3 など AWS 上で何ができるか

AWS:Trust Policy

IAM ロールの「信頼関係」タブに書く JSON が Trust Policy です。本番ロールなら main ブランチからだけ、という条件を書きます。

{
  "Effect": "Allow",
  "Principal": {
    "Federated": "arn:aws:iam::123456789012: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:my-org/my-repo:ref:refs/heads/main"
    }
  }
}

sub の形式は repo:<owner>/<repo>:ref:refs/heads/<branch> です。PR プレビュー用ロールなら refs/pull/* など、ロールごとに Trust Policy を分けます。GitHub Environment を使う場合は repo:my-org/my-repo:environment:production 形式になる。

GitHub:workflow

Trust Policy で入口を決めたうえで、workflow 側で引き受ける IAM ロール ARN を指定します。

jobs:
  deploy-prod:
    if: github.ref == 'refs/heads/main'
    environment: production
    permissions:
      id-token: write   # JWT 取得に必要
      contents: read
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-prod
          aws-region: ap-northeast-1

  deploy-preview:
    if: github.event_name == 'pull_request'
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-preview
          aws-region: ap-northeast-1

AWS:Permission Policy

IAM ロールの「アクセス権限」タブに付けるポリシーです。用語の Permission Policy のとおり、引き受け後に許す AWS 操作をここで決めます。本番・プレビューでロールを分けた場合は、それぞれに必要な Permission Policy を付けます。

まとめ

GitHub Actionsからクラウドへデプロイするとき、Access KeyをSecretsに置く方式は手軽ですが、長期シークレットの管理・ローテーション・権限の切り方に弱みがあります。

OIDCを使うと、GitHubに長期シークレットを預けずに済みます。jobごとのJWTとTrust Policyで認可を組み立て、AWS APIは短命シークレットで呼び出せます。


この記事をシェア:

前の記事
【Cursor】サブエージェントとバックグラウンド実行
次の記事
【AWS】SAA-C03を取りたいなら模試やるな、Ping-tやれ!