信頼ポリシーが正しいのにOIDCが拒否される
教科書どおりの信頼ポリシーなのに GitHub Actions からの AssumeRole が拒否される。CloudTrail で実トークンを見たら、新しめの org では sub クレームが repo:owner@ID/repo@ID:* という ID 入り形式だった。実クレームの確認手順(CloudTrail)と、両形式を併記する信頼ポリシー、gh api での ID 取得までの逆引きメモ。
TL;DR
- GitHub Actions の OIDC で AssumeRole が
Not authorizedになり、aud も sub も プロバイダも教科書どおりなら、実際に飛んでいる sub クレームを CloudTrail で見る - 新しめの GitHub org は sub が
repo:owner@ID/repo@ID:*という ID 入り形式。 従来形式repo:owner/repo:*の信頼ポリシーにはマッチしない - 対処は信頼ポリシーに両形式を併記すること。ID は
gh apiで取れる
症状
Error: Could not assume role with OIDC: Not authorized to perform sts:AssumeRoleWithWebIdentity
aws-actions/configure-aws-credentials がこれで落ちます。そして定番の確認は全部通る:
- OIDC プロバイダ
token.actions.githubusercontent.comはある。audience もsts.amazonaws.com - workflow の
permissions: id-token: writeもある - 信頼ポリシーの sub 条件も、動いている他リポジトリの実績ある形をコピーした
"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 はこの ID 入り形式で sub が発行される
- 古いアカウント・org は従来形式のまま
つまり「長年動いている個人リポジトリのロールとまったく同じ信頼ポリシーを、 新設 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 側で踏んだ他の罠は実機検証編へ。