GitHub ActionsのOIDCでAWS認証が通らない時にsubクレームを確認して信頼ポリシーを直してみた

GitHub ActionsのOIDCでAWS認証が通らない時にsubクレームを確認して信頼ポリシーを直してみた

GitHub ActionsからAWSへのOIDC認証が新しいリポジトリだけ失敗する場合、GitHubの仕様変更「immutable subject claim」が原因かもしれません。本記事では、認証エラーを再現してsubクレームを確認し、信頼ポリシーを修正する手順を紹介します。
2026.10.10

こんにちは、つくぼし(tsukuboshi0755)です!

GitHub ActionsからAWSへOIDCで認証する時、IAMロールの信頼ポリシーには組織名とリポジトリ名でsubの条件を書くのが定番でした。
ところが2026年7月15日より後に作成したリポジトリでは、この定番の書き方のままだとCIのAWS認証が通りません。
既存のリポジトリでは動いている設定なのに新しいリポジトリだけ落ちる時、何を疑えばよいか分からず止まってしまう事はないでしょうか?

そこで本記事では、その原因であるGitHubの仕様変更、immutable subject claimによる認証エラーを再現し、subクレームを確認して信頼ポリシーを直すまでの手順について紹介します!

immutable subject claimとは

従来のsubには、オーナー(組織またはユーザー)名とリポジトリ名がそのまま入っていました。
この形式には名前の再利用に伴う弱点があります。
リポジトリを削除・リネームした後に第三者が同じ名前を取得すると、そのリポジトリのワークフローが同一のsubを持つトークンを受け取れるため、AWS側のロールを引き受けられてしまいます。

https://github.blog/changelog/2026-04-23-immutable-subject-claims-for-github-actions-oidc-tokens/

この弱点を塞ぐため、新形式では名前に続けて不変の数値IDが入るようになりました。
主な変更点は以下の通りです。

項目 内容
新形式の構文 repo:OWNER@OWNER-ID/REPO@REPO-ID:ref:refs/heads/BRANCH
区切り文字に@を使う理由 GitHubのユーザー名とリポジトリ名に@が現れ得ないため
自動適用の対象 2026年7月15日より後に作成されたリポジトリ。同日より後のリネーム・移管も新形式になる
既存リポジトリ 明示的にオプトインした場合のみ新形式になる
オプトインの方法 Organization単位・リポジトリ単位のどちらでも、OIDC設定のUIかREST APIで切り替える

https://docs.github.com/ja/actions/reference/security/oidc

このシナリオを防ぐために、2026 年 7 月 15 日以降に作成されたリポジトリでは、所有者 ID とリポジトリ ID の両方を含む不変の既定のサブジェクト形式が使用されるようになりました。

構文: repo:OWNER@OWNER-ID/REPO@REPO-ID:ref:refs/heads/BRANCH

変更できないサブジェクト要求を選択しない限り、2026 年 7 月 15 日より前に作成されたリポジトリは以前の形式を維持します。 OIDC 設定 UI または REST API を使用して、組織またはリポジトリ レベルでオプトインできます。

2026 年 7 月 15 日以降に作成されたリポジトリ、または変更できないサブジェクト要求にオプトインしたリポジトリの場合、 sub 要求には、変更できない例に示すように owner_id と repo_id が含まれます。 リポジトリで使用されている形式に合わせて信頼ポリシーを更新します。

既存のリポジトリは旧形式のままなので、新しく作ったリポジトリだけが新形式のsubを受け取り、「特定のリポジトリだけ落ちる」という見え方になります。

なおsubから数値IDを外す事はできず、REST APIのinclude_claim_keysでクレームをカスタマイズしてもowner_idとrepo_idは残ります。
そのため対処は、信頼ポリシー側を新形式へ合わせる事になるでしょう。

ただし新形式に合わせた信頼ポリシーは、旧形式のsubを送る既存リポジトリとは一致しません。
同じテンプレートを既存リポジトリにも使っているなら、オプトインで新形式へ揃えるかを先に決めておくと良いでしょう。

前提条件

本記事のデモを再現するには、以下を用意してください。

  • CloudFormationでIAMロールとOIDCプロバイダを作成できる権限を持つAWSアカウント
  • 2026年7月15日より後に作成したGitHubリポジトリ(以降octo-org/octo-repoとします)
  • 上記リポジトリのデフォルトブランチがmainである事

なおOIDCプロバイダは同じURLのものを1つのAWSアカウントに複数作成できません。
既にtoken.actions.githubusercontent.comのプロバイダがあるアカウントでは、後述のテンプレートからGitHubOIDCProviderリソースを外し、既存プロバイダのARNを参照するよう書き換えてください。

本記事では以下のバージョンで検証しています。

項目 バージョン
aws-actions/configure-aws-credentials v6
GitHub Actionsランナー ubuntu-24.04

やってみた

まず従来通りの旧形式の信頼ポリシーでロールを作り、新しいリポジトリから認証が失敗する事を再現します。
その上でsubの実物を確認し、信頼ポリシーを新形式に直します。

旧形式の信頼ポリシーでIAMロールを作成する

OIDCプロバイダとIAMロールを、1つのCloudFormationテンプレートでまとめて作成します。
信頼ポリシーのsubは、これまで一般的だった組織名とリポジトリ名だけの旧形式で書いておきます。

bootstrap.yaml
AWSTemplateFormatVersion: "2010-09-09"

Parameters:
  GitHubOrg:
    Type: String
    Default: octo-org

  RepositoryName:
    Type: String
    Default: octo-repo

Resources:
  GitHubOIDCProvider:
    Type: AWS::IAM::OIDCProvider
    Properties:
      Url: https://token.actions.githubusercontent.com
      ClientIdList:
        - sts.amazonaws.com

  DemoRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: iamrole-for-oidc-demo
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Federated: !Ref GitHubOIDCProvider
            Action: sts:AssumeRoleWithWebIdentity
            Condition:
              StringEquals:
                token.actions.githubusercontent.com:aud: sts.amazonaws.com
                token.actions.githubusercontent.com:sub: !Sub repo:${GitHubOrg}/${RepositoryName}:ref:refs/heads/main

Outputs:
  RoleArn:
    Value: !GetAtt DemoRole.Arn

ロールには許可ポリシーを付けていません。
動作確認で呼ぶsts get-caller-identityは権限を必要としないため、認証が通るかどうかだけを確かめるには信頼ポリシーだけで足ります。

以下のコマンドでデプロイします。
GitHubOrgとRepositoryNameは、ご自身の組織名とリポジトリ名に置き換えてください。

aws cloudformation deploy \
  --template-file bootstrap.yaml \
  --stack-name github-oidc-demo \
  --parameter-overrides GitHubOrg=octo-org RepositoryName=octo-repo \
  --capabilities CAPABILITY_NAMED_IAM

デプロイ後、IAMコンソールでロールの「信頼関係」タブを開くと、subが旧形式で登録されている事を確認できます。

旧形式のsubが登録された信頼関係タブ

赤枠のsubは組織名とリポジトリ名を/で区切っただけの値で、数値IDは含まれていません。

ワークフローを作成する

リポジトリに以下のワークフローを追加します。
今回は検証用として、subクレームを確認する診断ステップDebug OIDC claimsも入れています。
role-to-assumeには、スタックの出力RoleArnの値を指定してください。

.github/workflows/oidc-demo.yml
name: OIDC demo

on:
  workflow_dispatch:

permissions:
  id-token: write
  contents: read

jobs:
  assume-role:
    runs-on: ubuntu-24.04
    steps:
      - name: Configure AWS credentials
        id: aws-credentials
        uses: aws-actions/configure-aws-credentials@v6
        with:
          role-to-assume: arn:aws:iam::<自アカウントID>:role/iamrole-for-oidc-demo
          aws-region: ap-northeast-1
          mask-aws-account-id: true

      - name: Debug OIDC claims
        if: failure() && steps.aws-credentials.outcome == 'failure' && runner.debug == '1'
        run: |
          TOKEN=$(curl -sS \
            -H "Authorization: bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \
            "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=sts.amazonaws.com" \
            | jq -r '.value')
          PAYLOAD=$(echo "${TOKEN}" | cut -d. -f2 | tr '_-' '/+')
          case $(( ${#PAYLOAD} % 4 )) in
            2) PAYLOAD="${PAYLOAD}==" ;;
            3) PAYLOAD="${PAYLOAD}=" ;;
          esac
          echo "${PAYLOAD}" | base64 -d | jq '{sub, aud, repository, ref}'

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

Debug OIDC claimsは、認証が失敗した時にsubの実物を確認するため、今回の検証用に入れた診断ステップです。
IDトークンは取得した時点でマスク対象になるため、ACTIONS_STEP_DEBUGを有効にするだけではsubがログに出てきません。
そこでACTIONS_ID_TOKEN_REQUEST_TOKENとACTIONS_ID_TOKEN_REQUEST_URLでトークンを取得し、ペイロード部分だけをデコードしています。

ログはリポジトリを読めるユーザーなら誰でも閲覧できるため、診断ステップにはログに残る情報を絞る工夫を2点入れています。

  • JWTは有効期限内ならそのまま認証に使えるため、トークン全体は出さず、cut -d. -f2で取り出したペイロードをjqで必要なクレームだけに絞る
  • 診断ステップは、認証が失敗し、かつデバッグ実行である時だけ動くようifで条件を付ける(参照のため、認証のステップにはidを付ける)

なお診断ステップは実運用のワークフローに常設せず、認証が通らない時に一時的に追加し、原因が分かったら外してください。

最後のGet caller identityは、ロールを引き受けられたかを確かめるためのステップです。
その出力にはAWSアカウントIDが含まれるため、Configure AWS credentialsにmask-aws-account-id: trueを指定し、ログ上でマスクしています。

認証エラーを再現する

リポジトリの「Actions」タブからOIDC demoワークフローを選び、「Run workflow」で実行します。
するとConfigure AWS credentialsのステップが、以下のエラーでリトライを繰り返した末に失敗します。

Retry AssumeRole: attempt 1 of 12 failed: Could not assume role with OIDC: Not authorized to perform sts:AssumeRoleWithWebIdentity. Retrying after 31ms.

リトライの末に失敗したConfigure AWS credentialsのログ

attempt 1 of 12の時点から同じ文言で拒否されており、ステップは失敗の表示で終わっています。

このエラー文言は、信頼ポリシーの条件が合わない場合にも、ロールに設定したIdPのARNが誤っている場合にも同じように返ってきます。
そのため、エラー文言だけでは原因を絞り込めません。

OIDC認証が通らない時に確認する箇所は、主に以下の3つです。

確認箇所 確認方法
ワークフローのpermissions id-token: writeが付いているか。付いていないとIDトークン自体を取得できない
信頼ポリシーのaud条件 トークンのaud(configure-aws-credentialsの既定はsts.amazonaws.com)と一致するか
信頼ポリシーのsub条件 トークンのsubと、StringEqualsなら完全一致、StringLikeならパターンで一致するか

今回のワークフローはpermissionsを付けており、audもテンプレート通りです。
残るsubは、GitHub側で何が送られているかを見なければ一致しているか判断できません。

デバッグ実行でsubクレームを確認する

失敗した実行の画面右上にある「Re-run jobs」から「Re-run all jobs」を選び、「Enable debug logging」にチェックを入れて再実行します。

Enable debug loggingにチェックを入れた再実行のダイアログ

チェックを入れて再実行すると、各ステップのログに##[debug]で始まるデバッグ用の行が加わります。

デバッグ実行ではrunner.debugが1になるため、認証の失敗後にDebug OIDC claimsステップが動きます。
新形式に該当していれば、出力は以下の通りです。

{
  "sub": "repo:octo-org@123456/octo-repo@456789:ref:refs/heads/main",
  "aud": "sts.amazonaws.com",
  "repository": "octo-org/octo-repo",
  "ref": "refs/heads/main"
}

診断ステップが出力したsubクレーム

repositoryは組織名/リポジトリ名のままで、数値IDが加わっているのはsubだけです。

対して信頼ポリシーのStringEqualsに登録されているのは、組織名とリポジトリ名だけの旧形式です。

repo:octo-org/octo-repo:ref:refs/heads/main

トークンのsubには組織名とリポジトリ名のうしろに@付きの数値が入っているため、完全一致になりませんでした。
audとrefのブランチは信頼ポリシーの条件と一致しており、残る差分はsubだけだと確定できます。

信頼ポリシーを新形式に修正する

信頼ポリシーを新形式へ合わせるには、subの条件に組織IDとリポジトリIDを埋め込む必要があります。
手元の検証では、2つのIDで扱いを分けてパラメータ化しました。

bootstrap.yaml
AWSTemplateFormatVersion: "2010-09-09"

Parameters:
  GitHubOrg:
    Type: String
    Default: octo-org

  GitHubOrgId:
    Type: String
    Description: 組織ID
    Default: "123456"
    AllowedPattern: ^[0-9]+$

  RepositoryName:
    Type: String
    Default: octo-repo

  RepositoryId:
    Type: String
    Description: リポジトリID。`*` を指定すると任意のリポジトリIDを受け付ける
    Default: "*"
    AllowedPattern: '^([0-9]+|\*)$'

Resources:
  GitHubOIDCProvider:
    Type: AWS::IAM::OIDCProvider
    Properties:
      Url: https://token.actions.githubusercontent.com
      ClientIdList:
        - sts.amazonaws.com

  DemoRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: iamrole-for-oidc-demo
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Federated: !Ref GitHubOIDCProvider
            Action: sts:AssumeRoleWithWebIdentity
            Condition:
              StringEquals:
                token.actions.githubusercontent.com:aud: sts.amazonaws.com
              StringLike:
                token.actions.githubusercontent.com:sub: !Sub repo:${GitHubOrg}@${GitHubOrgId}/${RepositoryName}@${RepositoryId}:ref:refs/heads/main

Outputs:
  RoleArn:
    Value: !GetAtt DemoRole.Arn

組織IDは一度調べれば済む値なので、パラメータのデフォルト値に実際の値を書き込む前提にしています。
一方でリポジトリIDは普段目にする機会が少なく、セットアップのたびに各自へ調べさせるのは現実的でないため、デフォルトを*にしました。

ただし*のままでは、同じ組織で同じ名前のリポジトリを作り直すと、IDが変わっても条件を通過します。
StringLikeはワイルドカードを含まない値を完全一致で照合するため、厳密に締めたい環境ではリポジトリIDに実際の数値を渡してください。

またStringLikeの*は/を含む任意の文字列に一致するため、12*のような値を埋めると別のIDまで通ってしまいます。
RepositoryIdにAllowedPatternを付け、受け付ける値を数値列か*単体に制限しているのはこのためです。

IDはGET /repos/{owner}/{repo}のレスポンスのidとowner.idで確認できますが、デモなら診断ステップのsubに出た値を使うのが手軽でしょう。

テンプレートを修正したら、組織IDを渡してスタックを更新してください。

aws cloudformation deploy \
  --template-file bootstrap.yaml \
  --stack-name github-oidc-demo \
  --parameter-overrides GitHubOrg=octo-org GitHubOrgId=123456 RepositoryName=octo-repo \
  --capabilities CAPABILITY_NAMED_IAM

更新後にロールの「信頼関係」タブを開くと、subの条件がStringLikeに移り、ID入りの新形式になっている事を確認できます。

新形式のsubに直した信頼関係タブ

組織IDは数値で固定され、リポジトリIDの位置だけが*になっています。

動作確認

信頼ポリシーを修正した後、「Run workflow」からワークフローを実行します。
今度はConfigure AWS credentialsが成功し、Get caller identityステップの出力は以下の通りになります。

{
    "UserId": "AROAXXXXXXXXXXXXXXXXX:GitHubActions",
    "Account": "***",
    "Arn": "***"
}

認証に成功したGet caller identityのログ

mask-aws-account-idによって、AccountとアカウントIDを含むArnは値全体が***に置き換わっています。
ロールを引き受けられた根拠は、Configure AWS credentialsの成功と、UserIdがロールを表すAROAで始まる値になっている事です。

認証が成功しているため、Debug OIDC claimsステップはスキップされます。
通常の実行ではクレームがログに残らない事も、この画面で確認できます。

最後に

今回は、GitHub ActionsのOIDCでAWS認証が通らない原因であるimmutable subject claimによるエラーを再現し、subクレームを確認して信頼ポリシーを直すまでの手順について紹介しました。

新しいリポジトリだけ認証が通らない時は、テンプレートやワークフローではなく、GitHubが発行するsubの形式が変わっているのかもしれません。
IDトークンはログでマスクされるため、subの実物を見られるかどうかで原因の切り分けにかかる時間が大きく変わります。

同じように新しいリポジトリだけ認証が通らずに困っている方は、ぜひ本記事の診断ステップでsubの実物を確認するところから試してみてください!

以上、つくぼし(tsukuboshi0755)でした!

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事