- 1 1. なぜ Cross-Account設計で詰まるか — Vol1-3からの架橋とシリーズ完結告知
- 2 2. STS全体像 — AssumeRole / GetSessionToken / GetFederationToken / DecodeAuthorizationMessage 4種API完全解説
- 3 3. 信頼ポリシー深掘り — Principal × Condition 完全マトリクスで最小権限を実現する
- 4 4. Cross-Account パターン4種 — 一方向 / 双方向 / Hub-Spoke / マルチHop 設計と適用シーン
- 5 5. Confused Deputy 問題と対策 — ExternalId / SourceAccount / SourceArn の3つの盾
- 6 6. 詰まりポイント7選 図解 — Cross-Account で詰まる7つの落とし穴
- 7 7. アンチパターン→正解パターン変換演習 — 5問で実践力を身につける
- 8 8. まとめ + IAM入門4巻シリーズ完結告知 + 次シリーズ予告
1. なぜ Cross-Account設計で詰まるか — Vol1-3からの架橋とシリーズ完結告知
- Vol1: IAMポリシー設計入門 — 必要な権限の特定から最小権限設計まで
- Vol2: 複数アカウント時代のIAM設計 — Organizations × Identity Center 完全実践
- Vol3: IAM権限棚卸し自動化 — Access Analyzer × CloudTrail Lake で継続運用
- Vol4 (本記事): STS × Cross-Account 実践 — 信頼ポリシー深掘りと Confused Deputy 対策
1-1. IAM入門シリーズの旅路
IAM入門シリーズは Vol1 から Vol3 まで、「権限をどう設計し、組織に展開し、継続的に保ち続けるか」という3段階のテーマを積み重ねてきた。
Vol1 (IAMポリシー設計入門) では、IAMの根幹となる「権限の書き方」を学んだ。5レイヤーの評価ロジック (Explicit Deny → SCP → Permission Boundary → Identity-based → Resource-based) を理解し、Access Analyzer を活用した最小権限の特定法を習得した。JSON ポリシーの構造、Action/Resource/Condition の組み合わせ、インラインポリシーとマネージドポリシーの使い分けまで、単一アカウント IAM の基礎を固めた。
Vol2 (複数アカウント時代の IAM 設計) では、Organizations と Identity Center を組み合わせた「権限をどこに配るか」を学んだ。SCP によるガードレール設計、Permission Boundary による権限委譲、Identity Center のPermission Set でマルチアカウントへの一括配布する仕組みを理解した。単一アカウントの IAM が「縦の権限管理」なら、Vol2 は「横断的な権限統制」だ。
Vol3 (IAM権限棚卸し自動化) では、「作った権限を正しく保ち続ける」運用自動化を学んだ。IAM Access Analyzer の Policy Generation と未使用アクセス検出、CloudTrail Lake による API 実績分析、EventBridge × Lambda を使った月次棚卸しパイプラインを習得した。権限は時間とともに肥大化する——それを防ぐのが Vol3 の主題だった。
そして Vol4 (本記事) では、「アカウントをまたいで権限を委譲する仕組み」を深掘りする。STS (AWS Security Token Service) が Cross-Account アクセスの中核技術だ。AssumeRole 一行で「A アカウントのリソースから B アカウントのリソースを操作できる」が、なぜ詰まるのか——その理由はシンプルで深い。
なぜ Cross-Account 設計で詰まるか:
AssumeRole が他の IAM 操作と本質的に異なる点は、権限の確認が2箇所で行われることだ。
呼ぶ側 (Account A) ───────────────────────────────→呼ばれる側 (Account B)
IAM ポリシーSTS AssumeRole API 信頼ポリシー (Trust Policy)
(sts:AssumeRole 許可?)(Account A を Principal に許可?)
↓ ↓
許可が必要許可が必要 (両方揃って初めて成功)
呼ぶ側の IAM ポリシーで sts:AssumeRole を許可しても、呼ばれる側のロールの信頼ポリシーに呼ぶ側の Principal が登録されていなければ失敗する。逆も同じだ。この「両側の一致」という概念が単一アカウント IAM になかった感覚であり、最大の詰まりポイントになる。
加えて、信頼ポリシーの Principal には複数の種類がある。AWS (IAM ユーザーやロール)、Service (EC2 や Lambda 等の AWS サービス)、Federated (SAML IdP や OpenID Connect) で構文がそれぞれ異なり、意図した Principal を正確に指定しないと不必要に広い信頼範囲になる。さらに Condition キーで絞り込まないと、Confused Deputy 攻撃のリスクが生まれる——これが Vol4 最大のテーマだ。
1-2. 本記事で学ぶこと
本記事は次の5段階でCross-Account設計の全体像を習得できるように構成されている。
§2 STS 全体像: AssumeRole / GetSessionToken / GetFederationToken / DecodeAuthorizationMessage の4種APIを比較し、それぞれの用途・セッション有効期間・返却する認証情報の違いを整理する。「どの API を使えばいいか」が状況に応じてわかるようになる。
§3 信頼ポリシー深掘り: Principal 4種 (AWS / Service / Federated / Canonical) × Condition 4種 (ExternalId / SourceArn / SourceAccount / MFA) のマトリクスを通じて、信頼ポリシーの「書き分け」を習得する。最も複雑で最も重要なセクションだ。
§4 Cross-Account パターン4種: 「直接 AssumeRole」「Organization 内クロスアカウント」「サードパーティへの委譲 (ExternalId 必須)」「サービス連携 (Confused Deputy 対策必須)」の4パターンを Terraform コード付きで解説する。
§5 Confused Deputy 対策: なぜ ExternalId が必要なのか、SourceAccount / SourceArn との使い分けはどうするのかを攻撃シナリオから逆算して理解する。
§6-§7 詰まりポイントとアンチパターン演習: 現場で頻発する7つの詰まりポイントを図解し、アンチパターン演習5問で理解を実践レベルまで引き上げる。
1-3. 前提条件
本記事は以下の前提知識を持つ読者を対象としている。
必須知識:
– IAM ポリシーの JSON 構造 (Action / Resource / Condition) が読めること。Vol1 の §2-§4 を理解していれば十分だ。
– AWS アカウントが2つ以上ある環境、または Organizations 環境があること。実際に AssumeRole を試せる環境があると理解が深まる。
– AWS CLI の基本操作 (aws iam, aws sts assume-role) が実行できること。
あると望ましい知識:
– Vol2 の Organizations / SCP の概念を理解していること。§4 の「Organization 内クロスアカウント」パターンを深く理解するために役立つ。
– Terraform の基本 (terraform init / terraform plan / terraform apply まで実行できる)。本記事のコード例は Terraform で書いているが、コンソール手順と並行して示すため、Terraform を知らなくても本質は理解できる。
スコープ制限: 本記事のテーマは STS と IAM 信頼ポリシーの設計に絞る。マネージドセキュリティ系サービスの詳細は別シリーズで扱う。
ハンズオン環境の準備: 本記事のコード例を実際に動かすには、最低限以下を用意する。
– AWS CLI がインストール済みで aws configure が完了していること
– テスト用 IAM ユーザーまたはロールに iam:CreateRole / iam:PutRolePolicy / sts:AssumeRole の権限があること
– Terraform を試す場合は terraform コマンドがインストール済みであること (バージョン 1.0 以上推奨)
– Organizations 環境がない場合でも、2つの別アカウントがあれば §4 の大半の演習は実行できる
– 本番アカウントではなく、サンドボックス/学習用アカウントでの実施を強く推奨する
1-4. 現場でよく見る3つの詰まりシーン
理論の前に、現場でどんな場面で AssumeRole の設計に行き詰まるかを3つのシーンで示す。どれかに心当たりがある場合、本記事を読むことで解決策が見つかるはずだ。
シーン① — 「権限は付けたはずなのに AccessDenied」
Lambda から別アカウントの S3 バケットにアクセスしようとしたが、エラーになるケースだ。Lambda の実行ロールに s3:GetObject を付けているのに動かない。
An error occurred (AccessDenied) when calling the GetObject operation:
User: arn:aws:sts::111111111111:assumed-role/LambdaRole/function-name
is not authorized to perform: s3:GetObject on resource:
arn:aws:s3:::cross-account-bucket/data.csv
原因は2段階ある。まず Lambda ロールのポリシーに sts:AssumeRole が不足しているか、AssumeRole 後のロールに s3:GetObject が付いていない。次に S3 バケットが別アカウントにある場合、バケットポリシーで呼ぶ側の Principal を明示的に許可する必要がある。Lambda ロールだけに権限を付けてもバケットポリシーが拒否していれば AccessDenied になる。
シーン② — 「信頼ポリシーを設定したのに AssumeRole が失敗する」
Account A のロールが Account B のロールを AssumeRole しようとして失敗するが、信頼ポリシーは設定済みというケースだ。
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111111111111:root"
},
"Action": "sts:AssumeRole"
}]
}
arn:aws:iam::111111111111:root はアカウント全体を指す。しかし呼ぶ側のロールに sts:AssumeRole の許可がないと失敗する。信頼ポリシーは「呼ばれる側の許可」であり、「呼ぶ側の許可」は別途 IAM ポリシーで与える必要がある。この「両側の設定が必要」という非対称性が直感に反して詰まる最大の原因だ。
シーン③ — 「サービスロールを作ったが信頼ポリシーの書き方がわからない」
CloudFormation スタックセットのクロスアカウントデプロイや、CodePipeline のクロスアカウントデプロイを設定しようとすると、サービスプリンシパル (cloudformation.amazonaws.com 等) を信頼ポリシーに書く必要が出てくる。
{
"Principal": {
"Service": "cloudformation.amazonaws.com"
}
}
この Service プリンシパルを使う場合、Condition に aws:SourceAccount または aws:SourceArn を付けないと Confused Deputy 脆弱性が生まれる。CloudFormation のサービスエンドポイントは複数の顧客が共有しており、悪意のある別アカウントが自分のスタックを作成する際に、あなたのロールを使って操作を実行できる可能性がある。これが Confused Deputy 問題だ。
これらの3シーンに共通するのは「Trust Policy × IAM Policy の両側の設計」と「Principal の種類と Condition の使い分け」だ。§3 でマトリクス形式に整理し、§5 で Confused Deputy 対策の全体像を解説する。
1-5. Cross-Account が必要になる典型的なユースケース
AssumeRole による Cross-Account アクセスは、現代の AWS 運用において避けて通れない技術だ。どんな場面で必要になるかを整理する。
1. マルチアカウント環境での集中ログ収集
Organizations 環境では、本番/ステージング/開発アカウントをそれぞれ分離することがベストプラクティスだ。ログ収集アカウント (Log Archive) に全アカウントの CloudTrail ログを集約する際、各メンバーアカウントから Log Archive アカウントの S3 バケットに書き込む権限が必要になる。これはバケットポリシーと AssumeRole の組み合わせで実現する。
2. CI/CD パイプラインのクロスアカウントデプロイ
ツールアカウント (Shared Services) に CI/CD パイプラインを集約し、各環境 (本番/ステージング) にデプロイする構成が一般的だ。パイプラインから本番アカウントのリソースを更新するとき、本番アカウントに DeployRole を作成して AssumeRole で一時認証情報を取得してデプロイを実行する。
# CLI での AssumeRole 例 (本番アカウントのデプロイロールを引き受け)
aws sts assume-role \
--role-arn "arn:aws:iam::999999999999:role/DeployRole" \
--role-session-name "pipeline-deploy" \
--external-id "pipeline-prod-deploy" \
--query 'Credentials' --output json
3. サードパーティサービスへの権限委譲
コスト最適化ツールや監視サービスが自社アカウントのリソース情報を読み取るとき、長期アクセスキーを渡す代わりに AssumeRole を使う。このとき ExternalId を必ず設定する——これが Confused Deputy 攻撃への防御であり、§5 で詳しく解説する。
4. Lambda / ECS からの別アカウントリソースアクセス
データ処理基盤で、処理は Processing アカウントの Lambda が行い、データは Data アカウントの S3/DynamoDB に保存する構成がある。Lambda の実行ロールが Processing アカウントに存在し、Data アカウントのリソースに AssumeRole でアクセスする。
5. Config / Access Analyzer の組織全体スキャン
Organizations の委任管理者機能では、Audit アカウントが全メンバーアカウントをスキャンする。これも内部的には AssumeRole の仕組みで動いており、Vol3 の棚卸し自動化パイプラインを Organizations 全体に展開する際の基礎知識になる。
前提: 本記事は IAMポリシー設計入門 (Vol1)、複数アカウント時代のIAM設計 (Vol2)、IAM権限棚卸し自動化 (Vol3) を読んでいると理解がさらに深まります。シリーズ通読を推奨しますが、本記事単体でも STS × Cross-Account の実践知識を習得できます。
1-6. 本記事を読み終えたらできること
本記事を通じて、以下5つの実践スキルを習得できる。
- AssumeRole フローの完全理解: 呼ぶ側の IAM ポリシーと呼ばれる側の信頼ポリシーを両方正しく設計し、AccessDenied を自力でデバッグできるようになる。
- Principal × Condition マトリクスの書き分け: AWS/Service/Federated の3種 Principal に対して、ExternalId / SourceArn / SourceAccount / MFA の Condition を状況に応じて選択できる。
- Cross-Account パターン4種の実装: 直接 AssumeRole / Organizations 内 / サードパーティ委譲 / サービス連携の4パターンを Terraform で実装できる。
- Confused Deputy 問題の理解と防御: 攻撃シナリオを理解した上で、
aws:ExternalIdとaws:SourceArnを使った防御実装ができる。 - 詰まりポイントの自己解決: AccessDenied のエラーメッセージから原因を特定し、
aws sts get-caller-identityや CloudTrail を使って問題を切り分けられる。
IAM 入門シリーズ4巻を通じて、単一アカウントの IAM 基礎から始まり、マルチアカウント統制、運用自動化、そして Cross-Account 設計まで体系的に習得できる。本記事がその最終章だ。
これらのスキルは単独でも役立つが、組み合わせることで真の威力を発揮する。たとえば「Vol3 で月次棚卸しを自動化し (EventBridge → Lambda)、その Lambda が Audit アカウントから各メンバーアカウントに AssumeRole して未使用ロールを検出する」という構成は、Vol3 の棚卸し知識と本記事 (Vol4) の Cross-Account 設計知識を組み合わせた典型例だ。
読者がこのシリーズを完走した時、AWS IAM の設計について「なぜこうするのか」を説明できる実力が身についているはずだ。設計の根拠を持てることが、運用・拡張・レビュー全てにおいて重要になる。
2. STS全体像 — AssumeRole / GetSessionToken / GetFederationToken / DecodeAuthorizationMessage 4種API完全解説
AWS Security Token Service (STS) は、有効期限付きの一時認証情報を発行するサービスです。「永続的なアクセスキーを発行せず、必要なときだけ短命な認証情報を使う」という設計思想の中核を担います。4種の API をそれぞれのユースケースに合わせて使い分けることが、Cross-Account 設計の第一歩です。
2-1. STS とは何か
STS の一時認証情報は以下の3要素で構成されます。
| 要素 | 説明 |
|---|---|
| AccessKeyId | ASIA で始まる一時的なアクセスキーID (永続的なアクセスキーは AKIA で始まる点で区別できる) |
| SecretAccessKey | アクセスキーに対応するシークレット |
| SessionToken | 一時認証情報を示すトークン。AWS SDK / CLI の認証ヘッダに含める必要がある |
この3要素は 全て揃って初めて機能 します。AccessKeyId と SecretAccessKey だけでは認証に失敗します。SDK の環境変数に設定する場合は AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN の3つを必ず設定してください。
STS が使われる主なシナリオ:
– クロスアカウントアクセス: アカウントAのリソースからアカウントBのロールを引き受けて操作する
– GitHub Actions / OIDC 連携: CI/CDパイプラインが静的なアクセスキーなしで AWS にアクセスする
– Lambda / EC2 の実行ロール: AWS サービスが内部的に STS を使って一時認証情報を取得する
– MFA 強制操作: IAM User が MFA 認証後に高権限操作を行う場合
STS は追加の設定なしに全 AWS アカウントで利用可能です。ただし、デフォルトでは一部のリージョンエンドポイントが無効になっている場合があるため、マルチリージョン構成では有効化設定を確認してください。
2-2. 4種APIの比較表
| API | 用途 | 呼び出し元 | 最大期間 | MFA 必須可否 |
|---|---|---|---|---|
| AssumeRole | IAM Role への権限委譲 | IAM User / Role / AWS Service | 12時間 (ロール設定依存) | 可 (Condition で MFA 必須化) |
| AssumeRoleWithWebIdentity | OIDC (GitHub Actions / Cognito 等) 経由の認証 | OIDC IdP / Cognito | 12時間 | 不可 |
| GetSessionToken | MFA 認証強化 | IAM User (MFA デバイス保有者) | 36時間 | 可 (MFA トークン必須) |
| GetFederationToken | 外部 IdP (SAML) 経由のフェデレーション | IAM User (with sts:GetFederationToken 権限) | 36時間 | 不可 |
| DecodeAuthorizationMessage | Access Denied エラーメッセージのデコード | IAM User / Role (with sts:DecodeAuthorizationMessage 権限) | N/A | 不可 |
DecodeAuthorizationMessage は一時認証情報の発行ではなく、アクセス拒否エラーのデバッグに使う特殊な API です。IAM ポリシーのデバッグ時に有用なため本章で合わせて解説します。
2-3. AssumeRole の詳細解説
AssumeRole は STS の中で最も重要な API です。「クロスアカウントアクセス」「Lambda / EC2 の実行ロール」「GitHub Actions の OIDC 連携の基盤」など、あらゆる権限委譲シナリオの中核を担います。
AssumeRole の仕組み
呼び出し元が sts:AssumeRole を呼ぶと、STS は対象ロールの Trust Policy を検証します。Trust Policy の Principal に呼び出し元が含まれており、かつ Condition が全て満たされていれば一時認証情報が発行されます。
[呼び出し元 (IAM User/Role)] ──sts:AssumeRole──> [STS]
↓ Trust Policy 検証 (Principal / Condition)
[対象 IAM Role]
↓ 許可
[呼び出し元] <── 一時認証情報 (AccessKeyId / SecretAccessKey / SessionToken) ──
CLI での AssumeRole
# 基本的な AssumeRole
aws sts assume-role \
--role-arn "arn:aws:iam::TARGET_ACCOUNT_ID:role/TargetRole" \
--role-session-name "my-session" \
--duration-seconds 3600
# 取得した認証情報を環境変数に一括設定
eval $(aws sts assume-role \
--role-arn "arn:aws:iam::TARGET_ACCOUNT_ID:role/TargetRole" \
--role-session-name "my-session" \
--query "Credentials.[AccessKeyId,SecretAccessKey,SessionToken]" \
--output text | awk '{
print "export AWS_ACCESS_KEY_ID="$1
print "export AWS_SECRET_ACCESS_KEY="$2
print "export AWS_SESSION_TOKEN="$3
}')
role-session-name は CloudTrail ログに記録されます。"github-actions-deploy-prod" のような意味のある名前を付けると、誰がどの目的でロールを引き受けたかを後から追跡しやすくなります。
AssumeRole のレスポンス構造
AssumeRole が成功すると以下の JSON 形式でレスポンスが返ります。
{
"Credentials": {
"AccessKeyId": "ASIAIOSFODNN7EXAMPLE",
"SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"SessionToken": "AQoDYXdzEJr...(省略)...token",
"Expiration": "2026-05-07T20:00:00Z"
},
"AssumedRoleUser": {
"AssumedRoleId": "AROAIOSFODNN7EXAMPLE:my-session",
"Arn": "arn:aws:sts::TARGET_ACCOUNT_ID:assumed-role/TargetRole/my-session"
}
}
AssumedRoleUser.Arn は CloudTrail に記録されます。{ロール名}/{セッション名} の形式になるため、セッション名に意味のある値を設定することで誰がいつどの目的でロールを引き受けたか追跡できます。
Terraform での AssumeRole (プロバイダー設定)
Terraform でクロスアカウントのリソースを管理する場合、provider ブロックに assume_role を設定します。
# 管理アカウント (デフォルトプロバイダー)
provider "aws" {
region = "ap-northeast-1"
}
# ターゲットアカウント (AssumeRole)
provider "aws" {
alias = "target"
region = "ap-northeast-1"
assume_role {
role_arn = "arn:aws:iam::${var.target_account_id}:role/TerraformDeployRole"
session_name = "terraform-${terraform.workspace}"
duration_seconds = 3600
# Confused Deputy 対策: sts:ExternalId を指定
external_id = var.external_id
}
}
# ターゲットアカウントのリソースはプロバイダーエイリアスを指定
resource "aws_s3_bucket" "cross_account" {
provider = aws.target
bucket= "my-cross-account-bucket"
}
session_name に terraform.workspace を含めることで、Terraform workspace (dev/stg/prod) ごとにセッション名が異なり、CloudTrail ログでの操作区別が容易になります。
AssumeRoleWithWebIdentity (OIDC 連携)
GitHub Actions などの OIDC IdP を使う場合は AssumeRoleWithWebIdentity を使います。静的なアクセスキーをシークレットに保存する必要がないため、GitHub Actions での AWS 認証のベストプラクティスとして広く採用されています。
# GitHub Actions ワークフローでの OIDC 認証例
jobs:
deploy:
permissions:
id-token: write# OIDC トークンの発行を許可
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::TARGET_ACCOUNT_ID:role/GitHubActionsRole
aws-region: ap-northeast-1
role-session-name: github-actions-${{ github.run_id }}
このワークフローは内部的に AssumeRoleWithWebIdentity を呼び出します。Trust Policy 側では Condition に token.actions.githubusercontent.com:sub (リポジトリ名 + ブランチ) を指定して、特定のリポジトリ・ブランチからのみロールを引き受けられるよう制限します。
2-4. GetSessionToken の使いどころ
GetSessionToken は IAM User が MFA デバイスで認証した後に一時認証情報を取得する API です。「MFA を必須にした IAM ポリシー (aws:MultiFactorAuthPresent: true) で保護されたリソースにアクセスする」ためのステップとして使います。
MFA 強制の典型的なパターン
- IAM ポリシーで
aws:MultiFactorAuthPresent: "true"のConditionがない場合はアクセス拒否 - IAM User が
GetSessionTokenを MFA トークンとともに呼ぶ - 返却された一時認証情報を使ってリソースにアクセス (MFA 済みセッション扱い)
# MFA デバイス ARN と TOTP コードを指定して一時認証情報を取得
aws sts get-session-token \
--serial-number "arn:aws:iam::123456789012:mfa/myuser" \
--token-code "123456" \
--duration-seconds 43200
# 返却された認証情報を環境変数に設定
export AWS_ACCESS_KEY_ID="ASIA..."
export AWS_SECRET_ACCESS_KEY="..."
export AWS_SESSION_TOKEN="..."
# MFA 必須ポリシーで保護されたリソースにアクセス可能になる
aws s3 ls s3://mfa-protected-bucket/
--serial-number は IAM User に登録されている MFA デバイスの ARN です。以下のコマンドで自分の MFA デバイス ARN を確認できます。
# 自分の MFA デバイス ARN を確認
aws iam list-mfa-devices --user-name myuser
# 返却例
# MFADevices:
#- EnableDate: '2025-01-01T00:00:00+00:00'
# SerialNumber: arn:aws:iam::123456789012:mfa/myuser
# UserName: myuser
DecodeAuthorizationMessage — Access Denied デバッグツール
DecodeAuthorizationMessage は AccessDenied エラーメッセージを人間が読める形式にデコードする API です。IAM ポリシーのデバッグ時に役立ちます。
# Access Denied エラーに含まれるエンコードされたメッセージをデコード
aws sts decode-authorization-message \
--encoded-message "<AccessDeniedエラーに含まれるエンコードメッセージ>"
デコード結果には「どのポリシーが評価されて拒否されたか」「SCP が関与しているか」などの詳細情報が含まれます。ただし sts:DecodeAuthorizationMessage 権限がない場合はこの API 自体も拒否されるため、開発者ロールにはあらかじめこの権限を付与しておくことを推奨します。
開発者ロールへの付与例: "Action": "sts:DecodeAuthorizationMessage" を個別に許可するポリシーを添付するか、AWS 管理ポリシーの ReadOnlyAccess に含まれるため ReadOnly 権限を持つロールでは自動的に使用可能です。
GetSessionToken と AssumeRole の使い分け
| 比較軸 | GetSessionToken | AssumeRole |
|---|---|---|
| 取得する権限 | 現在の IAM User と同じ権限 | 対象ロールの権限 |
| 主な用途 | MFA 強制操作 | 権限委譲・クロスアカウント |
| MFA サポート | 必須 (目的そのもの) | オプション (Condition で設定) |
| ロールの切り替え | 不要 | 必要 (Trust Policy の設定が必要) |
現代の AWS 設計では IAM User の利用そのものが推奨されなくなりつつあり、代わりに IAM Identity Center + AssumeRole パターンが主流です。GetSessionToken は既存の IAM User 環境での MFA 強化手段として有効ですが、新規設計では IAM Identity Center への移行を検討することを推奨します。
sequenceDiagram
participant Caller as 呼び出し元 (IAM User/Role)
participant STS as AWS STS
participant Role as IAM Role (対象)
participant Resource as AWSリソース
Caller->>STS: sts:AssumeRole (RoleArn指定)
STS->>Role: 信頼ポリシーを検証 (Principal/Condition)
Role-->>STS: 許可
STS-->>Caller: 一時認証情報 (AccessKey/SecretKey/Token)
Caller->>Resource: 一時認証情報でAPIコール
Resource-->>Caller: レスポンス

- 鉄則1: SessionToken を忘れない。一時認証情報の3要素 (AccessKeyId / SecretAccessKey / SessionToken) は全て揃って初めて機能する。SessionToken を環境変数に設定し忘れると AccessDenied になる。
- 鉄則2: 期間は最小限にする。AssumeRole の duration-seconds はデフォルト3600秒 (1時間)。長期間の認証情報は漏洩リスクが高い。必要最小限の時間を設定する。
- 鉄則3: セッション名で追跡可能にする。role-session-name に “github-actions-deploy” のような意味のある名前を付けると CloudTrail ログでの追跡が容易になる。
3. 信頼ポリシー深掘り — Principal × Condition 完全マトリクスで最小権限を実現する
IAM ロールには2種類のポリシーが存在します。「何を許可するか」を定義する アイデンティティベースポリシー(許可ポリシー) と、「誰が引き受けられるか」を定義する 信頼ポリシー(Trust Policy) です。AssumeRole を使ったクロスアカウント設計では、この信頼ポリシーの設計ミスが最大のセキュリティリスクになります。
3-1. 信頼ポリシーとは
信頼ポリシーは IAM Role に付与する特殊なリソースベースポリシーです。「誰が(Principal)このロールを引き受けられるか」と「どんな条件で(Condition)引き受けられるか」を定義します。
基本構造:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::123456789012:role/CallerRole" },
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "unique-external-id"
}
}
}
]
}
信頼ポリシーと許可ポリシーは 独立して管理 されます。AssumeRole が成功するには「信頼ポリシーで引き受け元が許可されている」かつ「引き受け元のプリンシパルに sts:AssumeRole アクションの許可がある」という両方の条件が必要です。

AssumeRole シーケンス(mermaid01)
sequenceDiagram
participant C as 呼び出し元<br/>(IAM User/Role)
participant S as AWS STS
participant T as 引き受け先ロール<br/>(Trust Policy)
participant A as 許可ポリシー
C->>S: sts:AssumeRole(RoleArn, ExternalId)
S->>T: 信頼ポリシー評価<br/>Principal が C を許可?
T-->>S: Allow / Deny
S->>C: Allow の場合: 信頼ポリシー評価OK確認
S->>A: 許可ポリシー評価<br/>sts:AssumeRole が許可?
A-->>S: Allow / Deny
S-->>C: 一時認証情報<br/>(AccessKeyId / SecretAccessKey / SessionToken)
C->>S: 一時認証情報で AWS API 呼び出し
3-2. Principal 4種の完全解説
Principal: AWS(IAM エンティティ)
最も一般的なパターンです。別アカウントまたは同一アカウントの IAM エンティティ(User / Role)を指定します。
{
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/CallerRole"
}
}
指定できる ARN の種類:
| ARN パターン | 説明 | 推奨度 |
|---|---|---|
arn:aws:iam::ACCOUNT:root | アカウント全体(そのアカウントの全エンティティ) | ❌ 非推奨(広すぎる) |
arn:aws:iam::ACCOUNT:user/username | 特定の IAM ユーザー | ⚠️ 限定的な用途のみ |
arn:aws:iam::ACCOUNT:role/RoleName | 特定の IAM ロール | ✅ 推奨 |
arn:aws:sts::ACCOUNT:assumed-role/Role/Session | 特定セッションのみ | ✅ より限定的 |
root ARN はアカウント全体を意味するため、そのアカウントのすべての IAM エンティティがロールを引き受けられる状態になります。必ず特定のロール ARN で絞り込んでください。
Principal: Service(AWS サービス)
Lambda・EC2・ECS タスクなどの AWS サービスがロールを引き受ける場合に使用します。
{
"Principal": {
"Service": "lambda.amazonaws.com"
}
}
主要サービスプリンシパル一覧:
| サービス | プリンシパル |
|---|---|
| Lambda 関数 | lambda.amazonaws.com |
| EC2 インスタンス | ec2.amazonaws.com |
| ECS タスク | ecs-tasks.amazonaws.com |
| EventBridge | events.amazonaws.com |
| CloudFormation | cloudformation.amazonaws.com |
| CodeBuild | codebuild.amazonaws.com |
| CodeDeploy | codedeploy.amazonaws.com |
サービスプリンシパルは Condition で aws:SourceArn または aws:SourceAccount を使って呼び出し元を限定することを推奨します(Confused Deputy 問題の防止)。
Principal: Federated(OIDC / SAML)
GitHub Actions・Google Workspace・Cognito などの外部 IdP(Identity Provider)からのフェデレーションアクセスに使用します。
{
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
}
}
GitHub Actions OIDC の完全な信頼ポリシー例:
{
"Version": "2012-10-17",
"Statement": [
{
"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:myorg/myrepo:*"
}
}
}
]
}
sub クレームで repo:myorg/myrepo:* のように絞ることで、特定のリポジトリからの OIDC トークンのみを許可できます。* を使いすぎると組織内の他リポジトリからも AssumeRole できてしまうため、ref:refs/heads/main のようにブランチまで絞るとより安全です。
Principal: *(全エンティティ — 要注意)
{
"Principal": "*"
}
"Principal": "*" はインターネット上のあらゆるエンティティを意味します。Condition で厳格に絞り込まない限り、ロールが誰でも引き受けられるパブリックロールになります。通常の IAM ロールでは使用禁止です(S3 バケットポリシーなどのリソースベースポリシーで特定用途に使う場合を除く)。
3-3. Condition 4種の実例
信頼ポリシーの Condition は「誰が引き受けられるか」をさらに絞り込む重要な要素です。Principal を絞るだけでなく、Condition を組み合わせることで多層的な防御が実現できます。
aws:PrincipalArn — 特定 ARN のみ許可
Principal に root ARN を使わざるを得ない場合(例:OrganizationsのSCPと組み合わせる場合)、aws:PrincipalArn で実際の呼び出し元をさらに絞り込みます。
{
"Condition": {
"ArnLike": {
"aws:PrincipalArn": "arn:aws:iam::123456789012:role/AllowedRole"
}
}
}
ArnLike はワイルドカード(* と ?)をサポートしているため、arn:aws:iam::*:role/DeployRole のようにアカウントをまたいで同名ロールを許可することもできます。
sts:ExternalId — 第三者サービス向けの Confused Deputy 対策
SaaS プロバイダーや外部パートナーに対してロールを付与する際の必須 Condition です。ExternalId がないと Confused Deputy 攻撃 に脆弱になります。
{
"Condition": {
"StringEquals": {
"sts:ExternalId": "unique-external-id-shared-with-saas"
}
}
}
Confused Deputy 攻撃とは: 悪意ある SaaS プロバイダー(または侵害された SaaS)が、自分のアカウント ID を使って別の顧客のロールを不正に引き受けようとする攻撃です。ExternalId は「ロール ARN + ExternalId の組み合わせ」でのみ引き受けを許可するため、ロール ARN だけでは不十分な状況を補強します。
ExternalId の生成・管理ルール:
– SaaS プロバイダーが生成し、顧客ごとに一意の値を割り当てる
– UUID v4 形式が推奨(予測不可能な値)
– ロールを作成する際に SaaS の管理コンソールから提供される値を使う
– ExternalId は定期的に更新することを推奨
aws:SourceAccount — 同一組織内のアカウント限定
AWS サービスプリンシパルが AssumeRole する際に、呼び出し元のアカウント ID を限定します。
{
"Condition": {
"StringEquals": {
"aws:SourceAccount": "123456789012"
}
}
}
aws:SourceAccount は Lambda・EventBridge・SNS などのサービスが特定アカウントのリソースから呼び出される場合のみ機能します。複数アカウントを許可する場合は配列で指定します。
{
"Condition": {
"StringEquals": {
"aws:SourceAccount": ["111111111111", "222222222222"]
}
}
}
aws:SourceArn — 特定リソースからの呼び出しのみ許可
aws:SourceAccount よりさらに細かく、特定のリソース ARN からの呼び出しのみに限定します。
{
"Condition": {
"ArnLike": {
"aws:SourceArn": "arn:aws:lambda:ap-northeast-1:123456789012:function:MyFunction"
}
}
}
Lambda 実行ロールの信頼ポリシー例(SourceArn で特定関数のみ許可):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole",
"Condition": {
"ArnLike": {
"aws:SourceArn": "arn:aws:lambda:ap-northeast-1:123456789012:function:my-production-function"
}
}
}
]
}
3-4. Principal × Condition 完全マトリクス
| Principal の種類 | ExternalId | SourceAccount | SourceArn | PrincipalArn | 典型的ユースケース |
|---|---|---|---|---|---|
| AWS(他アカウントのロール) | ✅ 推奨 | ✅ 推奨 | — | — | クロスアカウント委譲 / SaaS 連携 |
| AWS(同一アカウントのロール) | — | — | — | ✅ 任意 | 同一アカウント内のロール切替 |
| Service(Lambda等) | — | ✅ 推奨 | ✅ 推奨 | — | Lambda / ECS / EventBridge 実行ロール |
| Federated(OIDC) | — | — | ✅ 必須(sub クレーム) | — | GitHub Actions / Cognito |
*(全エンティティ) | ✅ 必須 | ✅ 必須 | ✅ 必須 | 通常は使わない | 特殊ユースケースのみ |
マトリクスの読み方:
– ✅ 必須: この組み合わせでは Condition 未設定は危険
– ✅ 推奨: 設定することで多層防御が実現できる
– —: このケースでは通常使用しない
3-5. Terraform 実装例
クロスアカウント AssumeRole(ExternalId + SourceAccount)
data "aws_iam_policy_document" "cross_account_trust" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]
principals {
type = "AWS"
identifiers = ["arn:aws:iam::${var.caller_account_id}:role/CallerRole"]
}
condition {
test = "StringEquals"
variable = "sts:ExternalId"
values= [var.external_id]
}
condition {
test = "StringEquals"
variable = "aws:SourceAccount"
values= [var.caller_account_id]
}
}
}
resource "aws_iam_role" "cross_account" {
name= "CrossAccountRole"
assume_role_policy = data.aws_iam_policy_document.cross_account_trust.json
tags = {
Environment = "production"
ManagedBy= "terraform"
}
}
GitHub Actions OIDC(Federated + sub クレーム)
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}
data "aws_iam_policy_document" "github_actions_trust" {
statement {
effect = "Allow"
actions = ["sts:AssumeRoleWithWebIdentity"]
principals {
type = "Federated"
identifiers = [aws_iam_openid_connect_provider.github.arn]
}
condition {
test = "StringEquals"
variable = "token.actions.githubusercontent.com:aud"
values= ["sts.amazonaws.com"]
}
condition {
test = "StringLike"
variable = "token.actions.githubusercontent.com:sub"
values= ["repo:${var.github_org}/${var.github_repo}:ref:refs/heads/main"]
}
}
}
resource "aws_iam_role" "github_actions" {
name= "GitHubActionsRole"
assume_role_policy = data.aws_iam_policy_document.github_actions_trust.json
}
- 鉄則1: Principal に
*を使わない。Condition で絞っても意図せず広い範囲にロールを公開するリスクがある。常に特定の ARN またはサービスプリンシパルを指定する。やむを得ずrootARN を使う場合はaws:PrincipalArnCondition で追加制限を必ず設ける。 - 鉄則2: 第三者サービスには必ず ExternalId を要求する。SaaS プロバイダーや外部パートナーがロールを引き受ける場合、ExternalId がないと Confused Deputy 攻撃に脆弱になる。SaaS 側が提供する一意の UUID を ExternalId として設定し、ロール ARN だけでは引き受けられない構成にする。
- 鉄則3: AWS サービスプリンシパルには SourceArn または SourceAccount を設定する。Lambda / EventBridge 等が AssumeRole する信頼ポリシーには、呼び出し元リソースの ARN またはアカウント ID を Condition で指定する。これにより同じサービスプリンシパルを使う他のリソースからの意図しない引き受けを防止できる。
4. Cross-Account パターン4種 — 一方向 / 双方向 / Hub-Spoke / マルチHop 設計と適用シーン
複数の AWS アカウントをまたいだリソースアクセスを設計するとき、関係するアカウント数・アクセス方向・管理の集中度によって適切なパターンが変わる。本章では 一方向・双方向・Hub-Spoke・マルチHop の 4 パターンを比較し、それぞれの Terraform 実装と適用シーンを解説する。
4-1. パターン概要表
Cross-Account アクセスパターンを選ぶ際は「アクセス方向」と「アカウント数・管理集中度」の 2 軸で検討する。
| パターン | 構造 | 適用シーン | 複雑度 | 監査性 | 管理負荷 |
|---|---|---|---|---|---|
| 一方向 | A → B | 開発アカウントから本番リソース読取 | 低 | 高 | 低 |
| 双方向 | A ⇄ B | 共有サービス (ログ集約・Config等) の相互利用 | 中 | 中 | 中 |
| Hub-Spoke | 管理アカウント → 複数スポーク | Organizations 中央集権管理・監査 | 中 | 高 | 中 |
| マルチHop | A → B → C | 3段階以上の委譲チェーン | 高 | 低 | 高 |
選択の基本方針:
- 単純なクロスアカウントアクセスは 一方向 を選ぶ。最もシンプルで CloudTrail での追跡も容易だ。
- Organizations で複数アカウントを中央管理するなら Hub-Spoke が適切だ。
- マルチHop は原則回避する。CloudTrail でのトレースが複雑になり、最小権限の維持も難しい。
4-2. 一方向パターン — A → B
最もシンプルな Cross-Account 設計。Account A のロールが Account B のリソースを AssumeRole で利用する。
[Account A (呼び出し元)] ──AssumeRole──> [Account B (ターゲット)]
Account B 側 (ターゲット): 信頼ポリシーの設定
Account A の特定ロールのみを信頼する Principal を設定する。
# Account B: ターゲットロール (Account A からの AssumeRole を許可)
data "aws_iam_policy_document" "one_way_trust" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]
principals {
type = "AWS"
identifiers = ["arn:aws:iam::${var.account_a_id}:role/CallerRole"]
}
condition {
test = "StringEquals"
variable = "sts:ExternalId"
values= [var.external_id]
}
}
}
resource "aws_iam_role" "target_role" {
name= "TargetRole"
assume_role_policy = data.aws_iam_policy_document.one_way_trust.json
}
resource "aws_iam_role_policy_attachment" "target_s3_read" {
role = aws_iam_role.target_role.name
policy_arn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
}
Account A 側 (呼び出し元): AssumeRole の実行
# Account A の CLI から Account B のロールを引き受ける
CREDS=$(aws sts assume-role \
--role-arn "arn:aws:iam::ACCOUNT_B_ID:role/TargetRole" \
--role-session-name "dev-read-session" \
--external-id "unique-external-id" \
--query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]' \
--output text)
export AWS_ACCESS_KEY_ID=$(echo $CREDS | awk '{print $1}')
export AWS_SECRET_ACCESS_KEY=$(echo $CREDS | awk '{print $2}')
export AWS_SESSION_TOKEN=$(echo $CREDS | awk '{print $3}')
# Account B の S3 バケットにアクセス
aws s3 ls s3://prod-data-bucket/
一方向パターンのポイント:
ExternalIdを設定することで Confused Deputy 攻撃を防ぐ (§5 で詳述)- Account A の CallerRole には
sts:AssumeRole権限を付与しておく - セッション名 (
role-session-name) に呼び出し元を識別できる文字列を含めると CloudTrail での追跡が容易になる
セッションタグで監査証跡を強化する:
AssumeRole 時に --tags オプションを使うと、セッションに任意のタグを付与できる。これらのタグは CloudTrail ログの requestParameters.tags に記録され、「どのパイプラインや担当者が AssumeRole したか」を後から特定できる。
# セッションタグを付与して AssumeRole (CI/CD パイプラインの場合)
aws sts assume-role \
--role-arn "arn:aws:iam::ACCOUNT_B_ID:role/TargetRole" \
--role-session-name "github-actions-deploy" \
--external-id "unique-external-id" \
--tags Key=CallerSystem,Value=github-actions \
Key=Repo,Value=my-app \
Key=Environment,Value=prod \
--query 'Credentials' \
--output json
複数の CI/CD パイプラインや Lambda 関数が同一のクロスアカウントロールを使う場合でも、セッションタグで呼び出し元を区別できるため、誤操作の追跡やインシデント調査が大幅に効率化される。
4-3. 双方向パターン — A ⇄ B
Account A と Account B が互いのリソースにアクセスするパターン。ログ集約や共有 S3 バケットの相互利用などで使う。
[Account A] ──AssumeRole──> [Account B]
[Account B] ──AssumeRole──> [Account A]
実質的には「一方向パターンを 2 組設定する」構成だ。各アカウントにそれぞれ信頼ポリシーを持つロールを作成する。
# Account A 側: Account B を信頼するロール
data "aws_iam_policy_document" "account_a_trust" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]
principals {
type = "AWS"
identifiers = ["arn:aws:iam::${var.account_b_id}:role/AccountBRole"]
}
}
}
resource "aws_iam_role" "account_a_shared_role" {
name= "AccountASharedRole"
assume_role_policy = data.aws_iam_policy_document.account_a_trust.json
}
双方向パターンの注意点:
- 相互に AssumeRole できる構成は「どちらが呼び出し元か」が混同しやすい
- CloudTrail でのログを定期的にレビューし、意図しない方向のアクセスが発生していないか確認する
- 「なぜ双方向が必要か」を設計書に明記し、一方向に簡略化できないか常に検討する
4-4. Hub-Spoke パターン — 管理アカウントによる一元制御
Organizations 環境で最も推奨されるパターン。管理アカウント (Hub) が各メンバーアカウント (Spoke) に対して AssumeRole し、中央から運用管理を行う。
[管理アカウント (Hub)]
├──AssumeRole──> [Account 1 (Spoke)]
├──AssumeRole──> [Account 2 (Spoke)]
└──AssumeRole──> [Account N (Spoke)]
各スポークアカウント側の共通実装:
# Terraform モジュール: スポーク側の信頼ポリシー (全スポークで共通)
variable "hub_account_id" {
description = "管理アカウント (Hub) の AWS アカウント ID"
type = string
}
variable "hub_role_name" {
description = "Hub で AssumeRole に使うロール名"
type = string
default = "OpsRole"
}
data "aws_iam_policy_document" "hub_trust" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]
principals {
type = "AWS"
identifiers = [
"arn:aws:iam::${var.hub_account_id}:role/${var.hub_role_name}"
]
}
condition {
test = "Bool"
variable = "aws:MultiFactorAuthPresent"
values= ["true"]
}
}
}
resource "aws_iam_role" "spoke_ops_role" {
name = "SpokeOpsRole"
assume_role_policy= data.aws_iam_policy_document.hub_trust.json
max_session_duration = 3600
}
resource "aws_iam_role_policy_attachment" "spoke_read_only" {
role = aws_iam_role.spoke_ops_role.name
policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
}
このモジュールを Organizations 配下の全スポークで適用することで、管理アカウントから各スポークへの統一されたアクセスパスを構築できる。aws:MultiFactorAuthPresent 条件を追加することで、MFA 認証済みセッションからのみ AssumeRole を許可し、セキュリティを強化できる。
スポークアカウントへの AssumeRole は Operations チーム専用ロールからのみ許可し、開発者が直接スポーク本番環境を操作できないようにする設計が、Organizations 環境では一般的だ。
4-5. マルチHop パターンと設計上の注意点
A → B → C の 3 段階委譲。セッション B から再度 AssumeRole して C にアクセスする構成だ。
Account A ──AssumeRole──> Account B (中継) ──AssumeRole──> Account C
技術的には実現可能だが、以下の問題点があるため 原則として Hub-Spoke パターンに置き換えることを推奨する。
# Account C: Account B の中継ロールを信頼 (マルチHop 構成)
data "aws_iam_policy_document" "multi_hop_trust" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]
principals {
type = "AWS"
identifiers = ["arn:aws:iam::ACCOUNT_B_ID:role/RelayRole"]
}
}
}
マルチHop の問題点:
| 問題 | 詳細 |
|---|---|
| CloudTrail 追跡困難 | 最終呼び出し元 (Account A) が Account C のログから直接見えない |
| セッションポリシーの累積制限 | ホップごとにセッションポリシーが交差され、権限が意図せず制限される |
| 最小権限維持の困難 | 中継ロールに余分な権限が付与されやすい |
| 設計の複雑化 | 信頼関係の変更時に連鎖的な修正が必要になる |
推奨の代替設計: マルチHop が必要になった場合は Hub-Spoke パターンへの変更を検討する。管理アカウントから直接 Account C に AssumeRole する設計に変えることで、CloudTrail でのトレースが単純になる。
4-6. mermaid02: Hub-Spoke マルチアカウント AssumeRole フロー
graph LR
Hub["管理アカウント<br>(Hub)<br>OpsRole"]
S1["Account 1<br>(Spoke)<br>SpokeOpsRole"]
S2["Account 2<br>(Spoke)<br>SpokeOpsRole"]
S3["Account N<br>(Spoke)<br>SpokeOpsRole"]
CT["CloudTrail<br>集約バケット"]
Hub -->|"AssumeRole<br>+ MFA必須"| S1
Hub -->|"AssumeRole<br>+ MFA必須"| S2
Hub -->|"AssumeRole<br>+ MFA必須"| S3
S1 -->|"操作ログ"| CT
S2 -->|"操作ログ"| CT
S3 -->|"操作ログ"| CT
Hub-Spoke パターンでは各スポークの操作ログが CloudTrail 集約バケットに集まるため、管理アカウントから全アカウントの操作履歴を一元的に参照できる。これは監査対応やコスト最適化の観点でも重要だ。

- アカウント数が 2 つ、アクセス方向が固定: 一方向パターン。最もシンプルで監査しやすい。
- アカウント数が 2 つ、相互アクセスが必要: 双方向パターン。ただし本当に双方向が必要か設計時に再確認すること。
- Organizations で 3 アカウント以上を中央管理: Hub-Spoke パターン。管理アカウントに権限を集中させ、スポーク側のロールは最小権限に留める。
- マルチHop が必要になった場合: 設計を見直して Hub-Spoke に変更できないか検討する。やむを得ない場合のみマルチHop を採用し、CloudTrail の集約ログで定期的に監査する。
クロスアカウントロール チュートリアル → 公式ドキュメントを確認する
5. Confused Deputy 問題と対策 — ExternalId / SourceAccount / SourceArn の3つの盾

5-1. Confused Deputy 問題とは
Confused Deputy (混乱した代理人) 問題は、権限を持つ代理人 (AWS サービスや SaaS プロバイダー等) が攻撃者に悪用されるセキュリティ脆弱性だ。代理人自身は正当な権限を持っているが、「誰のために動くかを検証しない」ことが根本原因で、クロスアカウント設計を行う際に必ず意識すべき問題だ。
攻撃シナリオ — SaaS 経由の不正 AssumeRole
以下のシナリオを想定する。あなたはデータ分析 SaaS を利用しており、そのサービスが AWS へのアクセスを必要としている。
- あなた (被害者) は SaaS プロバイダーに AWS アカウントへのアクセスを許可するため、Trust Policy に
arn:aws:iam::111122223333:role/SaaSProviderRoleを Principal として登録する - 攻撃者も同じ SaaS を利用している (SaaS は全顧客に対して1つの IAM ロールを使い回している)
- 攻撃者が SaaS のリクエストパラメーターに被害者の RoleArn を埋め込み、「この RoleArn で処理せよ」と指示する
- SaaS は要求された RoleArn をそのまま
sts:AssumeRoleAPI に渡す — 被害者の Trust Policy はSaaSProviderRoleを許可しているため、AssumeRole が成功してしまう
脆弱な Trust Policy — ExternalId なし
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111122223333:role/SaaSProviderRole"
},
"Action": "sts:AssumeRole"
}
]
}
この Trust Policy の問題は「SaaSProviderRole を持つ任意のリクエストを受け入れる」点だ。SaaS を利用する攻撃者アカウントからも SaaSProviderRole を経由して AssumeRole されてしまい、被害者のロールへ不正アクセスできる。
なぜ問題になるか
Trust Policy は Principal (誰が引き受けるか) を検証するが、「誰のために SaaS が動いているか (どの顧客のリクエストか)」を検証しない。ExternalId がなければ、攻撃者は被害者の RoleArn を指定するだけで侵入できる。これが Confused Deputy 問題の本質だ。
この問題はマルチテナント SaaS だけでなく、複数顧客を持つすべてのサービスプロバイダーで発生しうる。
5-2. 3つの対策と使い分け早見表
Confused Deputy への対策は Principal の種類によって異なる。第三者 SaaS に対しては ExternalId、AWS サービス (Lambda / EventBridge 等) に対しては SourceArn / SourceAccount を使う。
| 対策 | Condition キー | 有効なケース | 限界 |
|---|---|---|---|
| ExternalId | sts:ExternalId | SaaS / 第三者サービスからの AssumeRole | SaaS 側が ExternalId を正しく実装していること前提 |
| SourceAccount | aws:SourceAccount | AWS サービス (Lambda / EventBridge 等) からの AssumeRole | アカウントレベルの制限のみ (特定リソースは SourceArn で絞る) |
| SourceArn | aws:SourceArn | 特定 AWS リソースからの AssumeRole | ARN が変わると Trust Policy の更新が必要 |
推奨の組み合わせ — シナリオ別
| シナリオ | 推奨 Condition の組み合わせ |
|---|---|
| SaaS / 外部パートナーが AssumeRole | sts:ExternalId + aws:SourceAccount |
| AWS サービス (Lambda / EventBridge) が AssumeRole | aws:SourceArn + aws:SourceAccount |
| GitHub Actions OIDC | token.actions.githubusercontent.com:sub (リポジトリ / ブランチ指定) |
| 同一アカウント内のサービス間 AssumeRole | aws:SourceArn のみで十分 |
ExternalId と SourceAccount を組み合わせると、攻撃者は ExternalId を知らない限り AssumeRole できない。さらに SourceAccount でアカウントも絞ることで二重の防御になる。AWS サービス系では SourceArn にアカウント ID が含まれるが、SourceAccount も合わせて指定することが AWS 公式推奨だ。
5-3. ExternalId の正しい実装
ExternalId は SaaS プロバイダーと顧客が共有する秘密の識別子だ。顧客ごとに一意で推測困難な値を使用することが必須条件で、UUID v4 が推奨される。全顧客で同じ ExternalId を共有する実装は Confused Deputy 問題を全く解決しないため、必ず顧客ごとに個別の値を生成すること。
Trust Policy (顧客側) — ExternalId 付き
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111122223333:role/SaaSProviderRole"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "a3f8c2d1-7b4e-4a9f-b2e6-1c0d5f3e8a7b"
}
}
}
]
}
この Trust Policy では、SaaS が正しい ExternalId を提示した場合のみ AssumeRole が成功する。攻撃者がこの値を知らない限り、たとえ SaaSProviderRole を持っていても AssumeRole は AccessDenied になる。
AWS CLI での AssumeRole 実行例 (SaaS 側の実装イメージ)
aws sts assume-role--role-arn "arn:aws:iam::999988887777:role/CustomerDataRole"--role-session-name "saas-customer-session"--external-id "a3f8c2d1-7b4e-4a9f-b2e6-1c0d5f3e8a7b"
ExternalId 生成・管理のポイント
- SaaS への初回接続時に UUID v4 を生成し、顧客ごとのデータストアに保存する
- 顧客が Trust Policy をセットアップする際に管理画面から ExternalId を表示する
- ExternalId はローテーションしない (変更すると顧客側の Trust Policy 更新が必要になり運用コストが増大する)
- ExternalId は秘密情報として扱うが、単体で認証に使わない —
Principalとの組み合わせが必須だ - ExternalId の値は英数字とハイフンのみ使用し、特殊文字を避けると実装上安全だ
- ExternalId を紛失・漏洩した場合は新しい UUID を生成し、顧客側の Trust Policy を更新してもらう (SaaS の管理画面から再発行フローを用意しておくと運用が楽になる)
- SaaS 側のコードレビューでは「
AssumeRole呼び出し時にExternalIdが必ず渡されているか」を必ずチェックする
5-4. AWS サービス向け — SourceArn + SourceAccount
AWS サービス (Lambda / EventBridge / S3 等) が別のロールを引き受ける場合、ExternalId は使えない。AWS サービスは ExternalId パラメーターを AssumeRole に渡せないからだ。代わりに aws:SourceArn と aws:SourceAccount を Condition に組み合わせる。
SourceArn は「どのリソースが AssumeRole を発行したか」を特定し、SourceAccount は「どのアカウントのリソースか」を二重に確認する。この組み合わせで、同じ AWS サービスエンドポイントを経由した他アカウントからの不正 AssumeRole を防ぐ。
Lambda 実行ロールの Trust Policy — SourceArn + SourceAccount
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole",
"Condition": {
"ArnLike": {
"aws:SourceArn": "arn:aws:lambda:ap-northeast-1:123456789012:function:MyDataProcessorFunction"
},
"StringEquals": {
"aws:SourceAccount": "123456789012"
}
}
}
]
}
ArnLike を使うとワイルドカードが使える。例: arn:aws:lambda:ap-northeast-1:123456789012:function:* で同一アカウントの全 Lambda 関数を許可する。特定関数のみに絞りたい場合は StringEquals の aws:SourceArn を使い完全一致で指定する。
SourceArn と SourceAccount を両方付ける理由
SourceArn にはアカウント ID が含まれているため一見冗長に見えるが、AWS の推奨では 両方の条件を同時に指定することが明記されている。SourceArn でワイルドカードを使う場合、SourceAccount がアカウント境界を明示的に保証するからだ。防御の多層化として、どちらか一方ではなく常に両方を指定する習慣を持つこと。
EventBridge ルールからの AssumeRole — Trust Policy 例
EventBridge Scheduler が Lambda 以外のロールを引き受ける場合も同様だ。スケジューラーが特定のターゲット (Step Functions や ECS タスク) を呼び出すために必要な実行ロールに対して、SourceArn + SourceAccount を設定する。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "scheduler.amazonaws.com"
},
"Action": "sts:AssumeRole",
"Condition": {
"ArnLike": {
"aws:SourceArn": "arn:aws:scheduler:ap-northeast-1:123456789012:schedule/default/MyDailyJob"
},
"StringEquals": {
"aws:SourceAccount": "123456789012"
}
}
}
]
}
Service プリンシパルは Lambda に限らず、EventBridge Scheduler / S3 / SNS / SES など、AWS サービスが AssumeRole を発行するすべてのケースで同様に設定できる。サービスの種類にかかわらず「SourceArn + SourceAccount のセット」というパターンを常に適用すること。
Terraform での Trust Policy 設定
data "aws_iam_policy_document" "lambda_assume_role" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["lambda.amazonaws.com"]
}
condition {
test = "ArnLike"
variable = "aws:SourceArn"
values= [
"arn:aws:lambda:ap-northeast-1:${var.account_id}:function:MyDataProcessorFunction"
]
}
condition {
test = "StringEquals"
variable = "aws:SourceAccount"
values= [var.account_id]
}
}
}
resource "aws_iam_role" "data_processor" {
name= "DataProcessorRole"
assume_role_policy = data.aws_iam_policy_document.lambda_assume_role.json
}
- ☑ SaaS / 外部サービスに AssumeRole を許可する Trust Policy には ExternalId を必ず設定する
- ☑ ExternalId は顧客ごとに一意の UUID v4 を使用し、全顧客で同じ値を共有しない
- ☑ AWS サービス (Lambda / EventBridge 等) の Trust Policy には SourceArn と SourceAccount を両方設定する
- ☑
Principal: "*"は使用しない — 必ず特定の Principal (ARN またはサービス) に絞る - ☑ Trust Policy を変更した後は
aws iam simulate-principal-policyで意図した通りの動作を確認する
6. 詰まりポイント7選 図解 — Cross-Account で詰まる7つの落とし穴
Cross-Account 設計を初めて実装する際、ほぼ全員が同じ箇所で詰まる。AWS ドキュメントを読んでも「なぜ AccessDenied が出るのか」を瞬時に特定できない原因は、失敗が2箇所の設定の組み合わせで起きるからだ。ここでは現場で頻出する7つの落とし穴を図解と対策コードで整理する。
詰まりポイント1: 信頼ポリシーと権限ポリシーの両方が必要なのを忘れる
AssumeRole が成功するには、呼び出し元と呼ばれる側の「両方」で権限が揃っている必要がある。どちらか一方だけ設定しても AccessDenied が返る。初学者が最も詰まるポイントだ。
- 誤解: 信頼ポリシー (Trust Policy) で AssumeRole を許可すれば使えると思う
- 実態: 呼び出し元のロールにも
sts:AssumeRoleアクションを許可するポリシーが必要 - 対策: 呼び出し元ロールに
sts:AssumeRoleを付与し、ターゲットロールの信頼ポリシーに Principal を設定する
設定が必要な箇所を JSON で確認する:
// 呼び出し元アカウントA: IAMポリシーに sts:AssumeRole を付与
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::TARGET_ACCOUNT:role/TargetRole"
}
]
}
// ターゲットアカウントB: 信頼ポリシーに呼び出し元を Principal として設定
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::CALLER_ACCOUNT:role/CallerRole" },
"Action": "sts:AssumeRole"
}
]
}
両方の設定が揃って初めて AssumeRole が成功する。片方しか設定されていない場合、どちらが欠けていても同じ AccessDenied が返る。CloudTrail の sts:AssumeRole イベントを確認し、errorCode と errorMessage に含まれる ARN でどちらが原因か特定する。
詰まりポイント2: Principal の ARN フォーマット誤り
信頼ポリシーの Principal に指定する ARN は、エンティティの種別によってフォーマットが異なる。一字でも誤ると認証に失敗するが、エラーメッセージには Principal が見つからないという旨は返らず、単純な AccessDenied になる。
- IAM ユーザー:
arn:aws:iam::123456789012:user/myuser - IAM ロール:
arn:aws:iam::123456789012:role/MyRole - AWS サービス:
lambda.amazonaws.com(ARN ではなくサービス識別子) - OIDC フェデレーション:
arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com
最も間違えやすいのは Assumed Role セッション ARN だ。aws sts get-caller-identity の Arn フィールドには arn:aws:sts::ACCOUNT:assumed-role/MyRole/session が返るが、この形式を信頼ポリシーの Principal に設定しても機能しない。
# 現在のアイデンティティ確認 — 返される Arn は信頼ポリシーの Principal には使えない
$ aws sts get-caller-identity
{
"UserId": "AROAXXXXXXXXXXXXXXXXX:session",
"Account": "123456789012",
"Arn": "arn:aws:sts::123456789012:assumed-role/MyRole/session"
}
// 誤り: Assumed Role セッション ARN は Principal に使えない (sts ドメインは不可)
{
"Principal": {
"AWS": "arn:aws:sts::123456789012:assumed-role/MyRole/session"
}
}
// 正解: IAM ロール ARN (sts ではなく iam ドメイン) を使う
{
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/MyRole"
}
}
対策としては、ARN をハードコードせず Terraform の data.aws_iam_role.<name>.arn を参照する設計にする。
詰まりポイント3: ExternalId を信頼ポリシーに設定したが呼び出し元が渡していない
ExternalId を Condition に設定した信頼ポリシーに対し、呼び出し元が --external-id を渡さずに AssumeRole を呼ぶと AccessDenied になる。SaaS 連携ロールや Confused Deputy 対策で ExternalId を追加した後に、既存の呼び出しコードを更新し忘れたときに発生する。
- 誤解: 信頼ポリシーに ExternalId Condition を追加したが、既存の呼び出しコードは変更していない
- 実態: ExternalId Condition が設定されていると、呼び出し側も必ず
--external-idを渡す必要がある - 対策: 信頼ポリシーに ExternalId を追加したら、呼び出し元のすべてのコードを同時に更新する
# ExternalId なし → AccessDenied
aws sts assume-role \
--role-arn "arn:aws:iam::TARGET:role/MyRole" \
--role-session-name "my-session"
# ExternalId あり → 成功 (信頼ポリシーの ExternalId 値と一致する場合)
aws sts assume-role \
--role-arn "arn:aws:iam::TARGET:role/MyRole" \
--role-session-name "my-session" \
--external-id "unique-customer-id-uuid"
Terraform でロールに ExternalId Condition を設定する場合は、変数として管理する:
# Terraform: ExternalId Condition を信頼ポリシーに設定
resource "aws_iam_role" "cross_account" {
name = "CrossAccountRole"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = { AWS = "arn:aws:iam::${var.saas_account_id}:role/SaaSRole" }
Action = "sts:AssumeRole"
Condition = {
StringEquals = { "sts:ExternalId" = var.external_id }
}
}
]
})
}
ExternalId の値はランダムな UUID を使い、SaaS プロバイダーごとに個別の値を設定することで Confused Deputy 攻撃を防ぐ。
詰まりポイント4: SessionToken を環境変数に設定していない
AssumeRole で取得した一時認証情報には AccessKeyId・SecretAccessKey・SessionToken の3つが含まれる。SessionToken を設定しないまま他の AWS API を呼ぶと InvalidClientTokenId エラーが返る。
- 誤解:
AccessKeyIdとSecretAccessKeyだけ設定すれば使えると思う - 実態: STS 一時認証情報では3変数すべての設定が必要
- 対策:
assume-roleの出力をパースして3変数を同時に設定する
# 誤り: SessionToken を設定していない → InvalidClientTokenId
export AWS_ACCESS_KEY_ID="ASIAXXXXXXXXXXXXXXXXX"
export AWS_SECRET_ACCESS_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# AWS_SESSION_TOKEN を設定していない
# 正解: jq で AssumeRole の出力から3変数を同時に設定
CREDENTIALS=$(aws sts assume-role \
--role-arn "arn:aws:iam::TARGET_ACCOUNT:role/MyRole" \
--role-session-name "my-session" \
--query "Credentials" \
--output json)
export AWS_ACCESS_KEY_ID=$(echo "$CREDENTIALS" | jq -r '.AccessKeyId')
export AWS_SECRET_ACCESS_KEY=$(echo "$CREDENTIALS" | jq -r '.SecretAccessKey')
export AWS_SESSION_TOKEN=$(echo "$CREDENTIALS" | jq -r '.SessionToken')
# 設定確認
aws sts get-caller-identity
Python SDK の場合は boto3.Session() に aws_session_token を渡すか、プロファイルベースの assume_role プロバイダーを使う方法が安全だ。環境変数を手動設定するとセッション有効期限の管理が難しくなるため、SDK の認証情報プロバイダーチェーンを活用することを推奨する。
詰まりポイント5: ロールのセッション期間を超えて使い続ける
AssumeRole で取得した一時認証情報には有効期限がある。デフォルトは1時間だ。長時間バッチ処理や Terraform Apply に使う場合、途中で認証情報が切れて ExpiredTokenException が発生する。
- 誤解: 一度 AssumeRole した認証情報でセッション中はずっと操作できる
- 実態: デフォルト1時間で期限切れ。ロール側の
MaxSessionDurationも確認が必要 - 対策:
--duration-secondsでセッション時間を延長し、ロール側のMaxSessionDurationも合わせて設定する
# デフォルト (3600秒=1時間)
aws sts assume-role \
--role-arn "arn:aws:iam::TARGET:role/MyRole" \
--role-session-name "batch-session"
# 最大時間に延長 (43200秒=12時間)
aws sts assume-role \
--role-arn "arn:aws:iam::TARGET:role/MyRole" \
--role-session-name "long-batch-session" \
--duration-seconds 43200
# ロールの MaxSessionDuration を確認
aws iam get-role --role-name MyRole \
--query "Role.MaxSessionDuration"
--duration-seconds の指定値がロール側の MaxSessionDuration を超えると DurationSecondsExceedMaxSessionDuration エラーになる。Terraform でロールを管理している場合は max_session_duration を合わせて設定する:
# Terraform: ロールの MaxSessionDuration を設定
resource "aws_iam_role" "cross_account" {
name = "LongSessionRole"
max_session_duration = 43200
assume_role_policy= data.aws_iam_policy_document.trust.json
}
長時間セッションが必要な場合は、EC2 Instance Profile や Lambda 実行ロールを直接使う設計に変更することも選択肢になる。これにより認証情報のローテーションを AWS に任せ、セッション管理の複雑さを排除できる。
詰まりポイント6: マルチHop で Permission の連鎖が切れる
A → B → C の3段階 AssumeRole (マルチHop) を構成したとき、A → B は成功するが B → C で AccessDenied になるケースがある。原因は B のロールポリシーに sts:AssumeRole が明示されていないことだ。
- 誤解: B のロールに S3 等のアクセス権限があれば、B のセッションから更に C に AssumeRole できる
- 実態: AssumeRole で取得したセッションは呼び出し元のロールポリシーの範囲内に制限される。B のポリシーに
sts:AssumeRoleがないと B のセッションは AssumeRole アクションを持たない - 対策: Hub-Spoke 設計に変更するか、各ロールに必要な
sts:AssumeRoleアクションを明示的に付与する
マルチHop の権限連鎖が切れる構造:
Account A (Role A)
│
│ AssumeRole (成功)
▼
Account B (Role B)
│
│ AssumeRole → Role B のポリシーに sts:AssumeRole がない → 失敗
▼
Account C (Role C)
// Role B の権限ポリシー — sts:AssumeRole を明示的に追加する必要がある
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::ACCOUNT_C:role/RoleC"
}
]
}
マルチHop は3段階以上になると権限追跡が難しくなる。Hub-Spoke 設計 (中継アカウントを集約アカウントに変更) を採用するか、ロールチェーンを2段階以内に収める設計を推奨する。
詰まりポイント7: ロールARNのアカウントIDを間違える
マルチアカウント環境では、各アカウントの12桁 Account ID を手入力する機会が多く、打ち間違いによる AccessDenied が発生しやすい。エラーメッセージには「そのロールが存在しない」旨の AccessDenied が返るだけで、Account ID の誤りとは明示されない。
- 誤解: 入力した ARN が正しいと思い込み、権限設定を何度も確認してしまう
- 実態: ARN 自体が誤っているため、信頼ポリシーが正しくても AssumeRole に失敗する
- 対策: Account ID はハードコードせず、Terraform の
dataブロックで動的に参照する
# 現在のアカウントID を確認するコマンド
aws sts get-caller-identity --query Account --output text
# ターゲットアカウントの Account ID を事前確認 (別プロファイル使用時)
aws sts get-caller-identity \
--profile target-account-profile \
--query Account \
--output text
# Terraform: Account ID をハードコードしない
data "aws_caller_identity" "target" {
provider = aws.target
}
data "aws_iam_role" "target_role" {
provider = aws.target
name = "TargetRole"
}
resource "aws_iam_policy" "assume_target" {
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect= "Allow"
Action= "sts:AssumeRole"
Resource = data.aws_iam_role.target_role.arn
}]
})
}
Account ID の管理には Terraform の data.aws_organizations_account を使うか、tfvars ファイルを環境 (本番/ステージング) ごとに分ける設計が堅牢だ。

- 呼び出し元を確認:
aws sts get-caller-identityで現在のアイデンティティを確認。意図したロール/ユーザーか? - 信頼ポリシーを確認: ターゲットロールの信頼ポリシーに Principal が正しく設定されているか確認。
- 呼び出し元の権限を確認: 呼び出し元のロールに
sts:AssumeRoleアクションが付与されているか確認。 - ExternalId の有無を確認: 信頼ポリシーに ExternalId Condition がある場合、呼び出し時に
--external-idを渡しているか確認。 - エラーメッセージを精読: AccessDenied のメッセージに含まれる ARN とポリシー名がヒントになる。
7. アンチパターン→正解パターン変換演習 — 5問で実践力を身につける
理論を身につけた後は「壊れた設定を直す」演習が定着を促す。アンチパターンの信頼ポリシーや権限ポリシーを見て、どこが問題かを特定し正解パターンに修正する5問の演習を用意した。JSON 形式と Terraform (HCL) 形式の両方で解答を確認し、実装力を高めよう。
演習1: Principal が広すぎる (アカウント root)
アンチパターン — アカウント root を Principal に設定すると、そのアカウントのすべての IAM エンティティが AssumeRole できてしまう:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::CALLER_ACCOUNT:root" },
"Action": "sts:AssumeRole"
}
]
}
正解パターン — 特定のロールに絞り、aws:SourceAccount Condition を追加する:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::CALLER_ACCOUNT:role/SpecificRole" },
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": { "aws:SourceAccount": "CALLER_ACCOUNT" }
}
}
]
}
解説: Principal に :root を使うと、そのアカウントの全ユーザー・ロールがターゲットロールを引き受けられる。実際に AssumeRole できるエンティティを特定のロール ARN に限定することで、最小権限の原則を守る。
演習2: ExternalId なしの SaaS 信頼ポリシー
アンチパターン — ExternalId がない SaaS 連携ロールは Confused Deputy 攻撃に脆弱だ:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::SAAS_ACCOUNT:role/SaaSRole" },
"Action": "sts:AssumeRole"
}
]
}
正解パターン — sts:ExternalId Condition を必ず追加する:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::SAAS_ACCOUNT:role/SaaSRole" },
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "unique-customer-id-a1b2c3d4"
}
}
}
]
}
解説: SaaS プロバイダーのロール ARN は複数の顧客が共有する。ExternalId がない場合、悪意のある同一 SaaS 利用者が別の顧客のロールを引き受ける Confused Deputy 攻撃が可能になる。ExternalId には顧客ごとにユニークな UUID を割り当て、SaaS プロバイダーとの間で安全に交換する。
演習3: Lambda ロールに SourceArn Condition がない
アンチパターン — Lambda サービスプリンシパルに Condition を設定しないと、アカウント内の全 Lambda 関数が AssumeRole できる:
data "aws_iam_policy_document" "lambda_trust" {
statement {
principals {
type = "Service"
identifiers = ["lambda.amazonaws.com"]
}
actions = ["sts:AssumeRole"]
# aws:SourceArn Condition なし → アカウント内の全 Lambda 関数が使用可能
}
}
resource "aws_iam_role" "lambda_role" {
name= "MyLambdaRole"
assume_role_policy = data.aws_iam_policy_document.lambda_trust.json
}
正解パターン — aws:SourceArn と aws:SourceAccount を Condition に追加して特定の Lambda 関数に限定する:
data "aws_iam_policy_document" "lambda_trust" {
statement {
principals {
type = "Service"
identifiers = ["lambda.amazonaws.com"]
}
actions = ["sts:AssumeRole"]
condition {
test = "ArnLike"
variable = "aws:SourceArn"
values= ["arn:aws:lambda:ap-northeast-1:${data.aws_caller_identity.current.account_id}:function:MyFunction"]
}
condition {
test = "StringEquals"
variable = "aws:SourceAccount"
values= [data.aws_caller_identity.current.account_id]
}
}
}
resource "aws_iam_role" "lambda_role" {
name= "MyLambdaRole"
assume_role_policy = data.aws_iam_policy_document.lambda_trust.json
}
解説: Lambda サービスプリンシパルは AWS の共有エンドポイントであるため、aws:SourceArn で特定の関数 ARN に絞ることが必須だ。ArnLike を使うことで関数名のワイルドカードマッチ (function:MyFunction*) も可能になる。
演習4: GitHub Actions OIDC で branch 制限がない
アンチパターン — aud だけを Condition にすると、GitHub の全リポジトリ・全ブランチから AssumeRoleWithWebIdentity できてしまう:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::ACCOUNT:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
}
}
}
]
}
正解パターン — 特定のリポジトリと main ブランチのみに制限する:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::ACCOUNT:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:myorg/myrepo:ref:refs/heads/main"
}
}
}
]
}
解説: sub Condition に repo:org/repo:ref:refs/heads/branch を設定することで、特定リポジトリの特定ブランチからのみ AssumeRoleWithWebIdentity を許可できる。PR からのデプロイを禁止したい場合は ref:refs/heads/main に限定する。StringLike と * を組み合わせると複数ブランチを許可できる。
演習5: マルチHop で Role B に sts:AssumeRole がない
アンチパターン — Role B の権限ポリシーに sts:AssumeRole が含まれていないため、B のセッションから Role C への AssumeRole が失敗する:
resource "aws_iam_role_policy" "role_b_policy" {
role = aws_iam_role.role_b.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect= "Allow"
Action= ["s3:GetObject", "s3:ListBucket"]
Resource = "*"
}
# sts:AssumeRole がない → Role B から Role C に AssumeRole できない
]
})
}
正解パターン — Role C への sts:AssumeRole を明示的に追加する:
resource "aws_iam_role_policy" "role_b_policy" {
role = aws_iam_role.role_b.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect= "Allow"
Action= ["s3:GetObject", "s3:ListBucket"]
Resource = "*"
},
{
Effect= "Allow"
Action= "sts:AssumeRole"
Resource = "arn:aws:iam::${var.account_c_id}:role/RoleC"
}
]
})
}
解説: AssumeRole で取得した一時認証情報のセッションポリシーは、呼び出し元のロールポリシーの範囲内に制限される。Role B のポリシーに sts:AssumeRole が明示されていないと、B のセッションは AssumeRole アクション自体を持たない。マルチHop を使う場合は各ロールに必要な sts:AssumeRole リソースを明示的に付与することが必須だ。
8. まとめ + IAM入門4巻シリーズ完結告知 + 次シリーズ予告
8-1. 本記事のまとめ
本記事では STS × Cross-Account 設計の実践に必要な知識を体系的に解説した。各章の要点を振り返り、知識を整理しよう。
STS の4種API の使い分け:
STS が提供する4種の API は、認証フローと使用シーンで明確に使い分ける。
AssumeRole: Cross-Account アクセスや権限の一時昇格に使う最も汎用的な API。呼び出し元の IAM ポリシーとターゲットロールの信頼ポリシーの両方に設定が必要GetSessionToken: MFA 強制が必要なシナリオで IAM ユーザーの一時認証情報を取得する。長期認証情報を持つユーザーが一時的な認証情報に変換するときに使うGetFederationToken: 企業の IdP (Active Directory 等) との連携でブローカー経由の認証情報を発行する。Session Policy によるスコープ制限が適用されるAssumeRoleWithWebIdentity: OIDC フェデレーションを使う GitHub Actions・EKS IRSA・Cognito に使用する。Web アイデンティティトークンを AWS の一時認証情報に交換する
信頼ポリシーの Principal × Condition マトリクスで最小権限を設計:
Principal の種別ごとに適切な Condition を使い分けることで最小権限を実現する。IAM ロールには aws:PrincipalArn で特定のロール ARN に制限し、AWS サービスプリンシパルには aws:SourceArn と aws:SourceAccount で Confused Deputy を防ぐ。OIDC プロバイダーには aud と sub を組み合わせて発行者・リポジトリ・ブランチを限定し、SaaS ロールには sts:ExternalId で顧客識別子を検証する。
Cross-Account パターン4種の選択基準:
- 一方向: シンプルな1対1アクセス。CI/CD パイプライン → 本番環境 デプロイが典型例
- 双方向: 2アカウント間の相互アクセス。共有サービスが互いのリソースを参照するケース
- Hub-Spoke: 複数メンバーアカウントを Audit/Shared Services アカウントが管理。Organizations 運用の標準形
- マルチHop: 3段階以上のロールチェーン。各ロールに
sts:AssumeRoleの明示付与が必須
Confused Deputy 問題と ExternalId / SourceAccount / SourceArn の3つの対策:
Confused Deputy とは、信頼されたサービスが悪意のある第三者の指示で意図しないリソースにアクセスする攻撃だ。3つの Condition でこれを防ぐ。sts:ExternalId は SaaS 連携時に顧客ごとのユニーク ID を突き合わせる。aws:SourceAccount はサービスプリンシパル使用時にアカウント ID で絞り込む。aws:SourceArn は特定リソース ARN で操作元を限定する。
7つの詰まりポイントとアンチパターン→正解パターン変換演習5問:
詰まりポイントの共通パターンは「両側の設定漏れ」「ARN フォーマット誤り」「期限・スコープの見落とし」の3種類に集約される。演習5問でアンチパターンを見抜く力と、JSON/Terraform 両形式での修正スキルを実践的に習得した。
Vol1からVol4まで読み進めた皆さん、お疲れ様でした。IAMの基礎から最小権限設計、マルチアカウント運用、権限棚卸し自動化、そして Cross-Account 実践まで — AWS IAM の全体像を4巻でカバーしました。
各巻の主要な習得項目を振り返りましょう:
- Vol1: IAMポリシー設計入門 — 最小権限の原則と必要な権限の特定方法
IAMポリシー5層評価ロジック (Explicit Deny → SCP → Permission Boundary → Identity-based → Resource-based)・Action/Resource/Condition の設計・Access Analyzer を使った最小権限の特定 - Vol2: 複数アカウント時代のIAM設計 — Organizations × Identity Center 完全実践
SCP によるガードレール設計・Permission Boundary による権限委譲・Identity Center Permission Set によるマルチアカウント一括配布・OU 階層設計 - Vol3: IAM権限棚卸し自動化 — Access Analyzer × CloudTrail Lake で継続運用
IAM Access Analyzer Policy Generation・未使用アクセス検出・CloudTrail Lake による API 実績分析・EventBridge × Lambda 月次自動化パイプライン - Vol4 (本記事): STS × Cross-Account 実践 — 信頼ポリシー深掘りと Confused Deputy 対策
AssumeRole × ExternalId の設計・Cross-Account パターン4種 (一方向/双方向/Hub-Spoke/マルチHop)・Confused Deputy 3種対策・詰まりポイント7選・アンチパターン演習5問
IAM入門シリーズを完走した方は、AWS アーキテクチャの基盤となるセキュリティ設計力を習得しています。ここで学んだ知識は EKS の IRSA、コンテナセキュリティ、マルチリージョン展開など、より高度なトピックで直接活用できます。
8-2. 次シリーズ予告
IAM入門シリーズ全4巻を完走した方の次のステップとして、Kubernetes 上での IAM 活用を深掘りするシリーズを予定している。EKS (Elastic Kubernetes Service) では IRSA (IAM Roles for Service Accounts) を使って Pod 単位で IAM ロールを割り当てる。Vol4 で学んだ OIDC フェデレーションの知識が IRSA の理解に直結するため、自然なステップアップになる。
次シリーズは AWS EKS 本番運用実践 を予定しています。Karpenter によるノード自動スケーリング、IRSA (IAM Roles for Service Accounts) による Pod 単位の最小権限設計、RBAC 設計まで実践的なコンテンツをお届けします。Vol4 で習得した OIDC フェデレーションの知識が IRSA の理解に直接つながります。ぜひお楽しみに。
EKS で IRSA を活用する実践ガイド
本巻で学んだ AssumeRoleWithWebIdentity の知識を EKS/IRSA に応用:
- EKS本番運用 Vol1: クラスタ設計 × IRSA × ALB Ingress (§4 IRSA設計実践)