信頼ポリシーが正しいのにOIDCが拒否される

教科書どおりの信頼ポリシーなのに GitHub Actions からの AssumeRole が拒否される。CloudTrail で実トークンを見たら、新しめの org では sub クレームが repo:owner@ID/repo@ID:* という ID 入り形式だった。実クレームの確認手順(CloudTrail)と、両形式を併記する信頼ポリシー、gh api での ID 取得までの逆引きメモ。

TL;DR

  1. GitHub Actions の OIDC で AssumeRole が Not authorized になり、aud も sub も プロバイダも教科書どおりなら、実際に飛んでいる sub クレームを CloudTrail で見る
  2. 新しめの GitHub org は sub が repo:owner@ID/repo@ID:* という ID 入り形式。 従来形式 repo:owner/repo:* の信頼ポリシーにはマッチしない
  3. 対処は信頼ポリシーに両形式を併記すること。ID は gh api で取れる

症状

Error: Could not assume role with OIDC: Not authorized to perform sts:AssumeRoleWithWebIdentity

aws-actions/configure-aws-credentials がこれで落ちます。そして定番の確認は全部通る:

"Condition": {
  "StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" },
  "StringLike":   { "token.actions.githubusercontent.com:sub": "repo:myorg/myrepo:*" }
}

実績ある形のコピーなのに落ちる、が今回の罠の性質です。

実際の sub を見る: CloudTrail に聞く

STS の拒否は「どのクレームで来たか」をエラーメッセージに出してくれません。 拒否した側の記録 — CloudTrail — に実物が残っています:

aws cloudtrail lookup-events --region ap-northeast-1 \
  --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRoleWithWebIdentity \
  --max-results 5

数分待って出てきたイベントの中身がこれでした:

error: AccessDenied Not authorized to perform sts:AssumeRoleWithWebIdentity
sub:   repo:rikukadev@328009292/kagerou-ssr-demo@1366497913:pull_request

repo:rikukadev/kagerou-ssr-demo:pull_request を待っていた信頼ポリシーに、 @数字 付きの sub が飛んできていました。マッチするわけがありません。

原因: 新しめの org は sub に不変 ID が入る

GitHub は org / リポジトリの名前再利用による成りすまし対策として、 sub クレームに不変 ID を含める形式を導入しています。ポイントは:

つまり「長年動いている個人リポジトリのロールとまったく同じ信頼ポリシーを、 新設 org のリポジトリ用に作ったのに動かない」という、再現条件が属人的な罠になります。 私はまさに、旧アカウント(従来形式で稼働中)の実績ポリシーをコピーして踏みました。

対処: 両形式を併記する

StringLike の値は配列にできるので、両方書いておけばどちらの形式でも通ります:

"Condition": {
  "StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" },
  "StringLike": { "token.actions.githubusercontent.com:sub": [
    "repo:myorg/myrepo:*",
    "repo:myorg@OWNER_ID/myrepo@REPO_ID:*"
  ]}
}

ID は API で取れます(リポジトリ 1 回の呼び出しで両方取れる):

REPO_ID=$(gh api repos/myorg/myrepo --jq .id)
OWNER_ID=$(gh api repos/myorg/myrepo --jq .owner.id)

ワイルドカードで repo:myorg@*/myrepo@*:* と書きたくなりますが、ID を素通しにすると 名前再利用対策という本来の意図が消えるので、実 ID を埋める方が筋です。

教訓

認証・認可の拒否は、拒否した側のログに実物を聞く。 トークンの中身を推測して 信頼ポリシーをいじり回すより、CloudTrail の 1 イベントを見るほうが速くて確実です。 GitHub Actions 側からは(ログを盛っても)sub の実物は出てきません。

この罠は kagerou(PR ごとのプレビュー環境 OSS)の 実機検証で踏んだものです。kagerou のセットアップスクリプトは上記の両形式併記を 自動でやります。経緯は kagerou の記事、 AWS 側で踏んだ他の罠は実機検証編へ。

← Back to all posts