GitHub Actions OIDC の仕組み — JWT で AWS を静的キーレス操作

GitHub Actions から AWS を操作する時、過去は IAM ユーザーのアクセスキーを GitHub Secrets に置く方式が主流でした。今は OIDC + AssumeRole でアクセスキーなしに一時クレデンシャルを取得する方式が標準。本記事ではその仕組みと、Terraform での IAM Role 設定を解説します。

静的アクセスキーの危険性

GitHub Secrets に AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY を入れる方式の問題点:

OIDC は これらをすべて構造的に解決 します。

OIDC + AssumeRole の流れ

処理は 5 ステップ:

  1. GitHub Actions のワークフロー実行時、OIDC ID トークン (JWT) を GitHub から発行
  2. JWT には sub: repo:owner/repo:ref:refs/heads/main 等の claim(主張)が入っている
  3. このトークンを AWS STS の AssumeRoleWithWebIdentity API に渡す
  4. AWS は IAM Role の Trust Policy で「このトークンは信頼できるか」を検証
  5. OK なら 1 時間有効 の AccessKey / SecretKey / SessionToken を返す

Runner はこの一時クレデンシャルを使って AWS API を叩きます。終わったらクレデンシャルは破棄。永続的なキーは存在しません。

JWT の claim を見てみる

GitHub が発行する OIDC JWT には次のような claim が含まれます:

{
  "iss": "https://token.actions.githubusercontent.com",
  "aud": "sts.amazonaws.com",
  "sub": "repo:iigtn/iigtn-platform:ref:refs/heads/main",
  "repository": "iigtn/iigtn-platform",
  "ref": "refs/heads/main",
  "ref_type": "branch",
  "actor": "<github-username>",
  "workflow": "frontend-deploy",
  "run_id": "12345678",
  ...
}

sub claim が「誰が・どこのリポジトリの・どのブランチから」を表します。これを Trust Policy で検証することで、「特定のリポジトリの main ブランチからの実行だけ許可」 が実現できます。

本シリーズの ci_oidc モジュール

このモジュールが作るリソースは 3 個:

  1. aws_iam_openid_connect_provider — GitHub を AWS の IdP として登録
  2. aws_iam_role — GitHub Actions が AssumeRole する役割
  3. aws_iam_role_policy — Role になった結果できる API(最小権限)

OIDC Provider の登録

resource "aws_iam_openid_connect_provider" "github" {
  url             = "https://token.actions.githubusercontent.com"
  client_id_list  = ["sts.amazonaws.com"]
  thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}
引数意味
urlGitHub の OIDC token 発行 URL(固定値)
client_id_listaudience。AWS STS は sts.amazonaws.com 固定
thumbprint_listGitHub IdP の TLS 証明書サムプリント。AWS が内部で自動検証する

1 アカウントに 1 個だけ作れば良い。複数のリポジトリで共有可能。

Trust Policy(誰が AssumeRole できるか)

Trust Policy は「この Role を AssumeRole できる Principal の条件」を定義:

data "aws_iam_policy_document" "trust" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRoleWithWebIdentity"]

    principals {
      type        = "Federated"
      identifiers = [aws_iam_openid_connect_provider.github.arn]
    }

    # 条件 1: aud (audience) は AWS STS のみ
    condition {
      test     = "StringEquals"
      variable = "token.actions.githubusercontent.com:aud"
      values   = ["sts.amazonaws.com"]
    }

    # 条件 2: sub (subject) は指定 owner/repo の指定ブランチのみ
    condition {
      test     = "StringLike"
      variable = "token.actions.githubusercontent.com:sub"
      values = [
        for branch in var.allowed_branches :
        "repo:${var.github_owner}/${var.github_repo}:ref:refs/heads/${branch}"
      ]
    }
  }
}

これにより:

Permissions Policy(Role になって何ができるか)

本シリーズの GitHub Actions は「フロントエンドのデプロイ」のみ行うので、必要最低限の権限:

data "aws_iam_policy_document" "deploy" {
  # S3: バケット情報の読み取り
  statement {
    actions   = ["s3:ListBucket", "s3:GetBucketLocation"]
    resources = [var.deploy_bucket_arn]                 # この 1 バケットだけ
  }

  # S3: オブジェクト操作 (sync で必要)
  statement {
    actions   = ["s3:GetObject", "s3:PutObject",
                 "s3:DeleteObject", "s3:PutObjectAcl"]
    resources = ["${var.deploy_bucket_arn}/*"]
  }

  # CloudFront: 特定 Distribution の Invalidation のみ
  statement {
    actions = [
      "cloudfront:CreateInvalidation",
      "cloudfront:GetInvalidation",
      "cloudfront:ListInvalidations"
    ]
    resources = [var.cloudfront_distribution_arn]       # この 1 Distribution だけ
  }
}

このポリシーでは 他のバケット・他の Distribution・IAM・Lambda・etc には一切触れない。万が一 Role が乗っ取られても被害が限定的。

IAM Role 本体

resource "aws_iam_role" "deploy" {
  name = var.role_name                                  # iigtn-github-actions-deploy
  description        = "GitHub Actions OIDC deploy role for ${var.github_owner}/${var.github_repo}"
  assume_role_policy = data.aws_iam_policy_document.trust.json
  tags               = var.tags
}

resource "aws_iam_role_policy" "deploy" {
  name   = "${var.role_name}-policy"
  role   = aws_iam_role.deploy.id
  policy = data.aws_iam_policy_document.deploy.json
}
IAM Role の description は ASCII / Latin-1 のみ。日本語を入れると ValidationError で apply 失敗します。本シリーズで実際にハマりました。

GitHub Actions ワークフロー側の最小実装

permissions:
  id-token: write       # ← OIDC token を発行するために必須
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::XXXXXXXXXXXX:role/iigtn-github-actions-deploy
          aws-region: ap-northeast-1

id-token: write permission を忘れると、OIDC token 発行が拒否されて AssumeRole が失敗します。これも初見でハマるポイント。

動作確認の見方

- name: Verify caller identity
  run: aws sts get-caller-identity

# 出力例
{
  "UserId": "AROAxxxxx:gha-frontend-deploy-12345",  ← Session ID 付き = AssumeRole 経由
  "Account": "XXXXXXXXXXXX",
  "Arn": "arn:aws:sts::XXXXXXXXXXXX:assumed-role/iigtn-github-actions-deploy/gha-frontend-deploy-12345"
}

ARN に assumed-role が含まれていれば「Role を AssumeRole した状態」が確認できます。

OIDC のメリット再掲

項目静的キーOIDC
有効期限無期限1 時間
漏洩時の被害大(永続)限定(1 時間で失効)
repo / branch 限定困難Trust Policy で完結
ローテーション手動毎回自動
監査CloudTrail で session 単位で追跡可能

静的キーをまだ使っている人は、今すぐ OIDC に移行する価値があります。本シリーズの実装をテンプレに使えば 30 分で切替可能。

次の記事

OIDC で IAM Role が用意できたところで、次の記事では実際の GitHub Actions ワークフロー YAML を書き、git push から CloudFront 反映までを自動化します。

📚 用語集

OIDC (OpenID Connect)
OAuth 2.0 の上に乗った認証プロトコル。ID トークン (JWT) を発行する標準仕様。
JWT (JSON Web Token)
Header / Payload / Signature の 3 部からなる署名付きトークン。Payload に claim を入れて、署名で改ざん検知する。
claim (クレーム)
JWT の Payload に入る「主張」。sub(誰)/ aud(audience)/ exp(有効期限)等。
sub claim
「subject = この JWT が誰を表すか」の claim。GitHub の場合 repo:owner/repo:ref:refs/heads/branch 形式。
aud claim (audience)
「この JWT は誰宛か」の claim。AWS STS 経由で使う場合は sts.amazonaws.com 固定。
STS (Security Token Service)
AWS の一時クレデンシャル発行サービス。AssumeRole 系 API がここで提供される。
AssumeRoleWithWebIdentity
OIDC や OAuth 等の Web Identity を使って IAM Role を Assume する STS API。
IdP (Identity Provider)
認証情報を発行する側。GitHub Actions OIDC では GitHub が IdP。
OIDC Provider (AWS IAM)
AWS 側で「この IdP を信頼する」と登録する IAM リソース。aws_iam_openid_connect_provider
Federated Principal
IAM の Principal タイプの 1 つで、外部 IdP からの認証を受け入れる。
Trust Policy (信頼ポリシー)
「この IAM Role を誰が AssumeRole できるか」を定義するポリシー。aws_iam_role.assume_role_policy に書く。
Permissions Policy (権限ポリシー)
「IAM Role になった結果、何ができるか」を定義するポリシー。Role に attach する。
最小権限 (Least Privilege)
「必要最小限の権限だけ与える」セキュリティ原則。漏洩時の被害を限定する。
Resource ARN
AWS リソースを一意に指す文字列。Permissions Policy の Resource 句で「このバケットだけ」「この Distribution だけ」と絞る。
thumbprint
TLS 証明書のフィンガープリント(ハッシュ)。OIDC Provider 登録時に GitHub IdP の thumbprint を指定する(AWS が自動検証)。
id-token: write permission
GitHub Actions のワークフロー permission の 1 つ。これが無いと OIDC token 発行が拒否される。OIDC を使う Job では必須。
aws-actions/configure-aws-credentials@v4
AWS が公式提供する GitHub Actions。OIDC で AssumeRole し、後続 step に一時クレデンシャルを env vars として注入。
session credentials
AssumeRole で発行される一時クレデンシャル。AccessKey / SecretKey / SessionToken の 3 点セット。有効期限あり。
CloudTrail
AWS の API コール履歴サービス。AssumeRole の session ID 単位で「誰が何を叩いたか」が追跡できる。