共通ロールのAssumeRoleで特定ユーザーだけに権限を足す方法(SourceIdentity)

2026年7月18日 に投稿 • 3 分 で読めます • 451 語
で共有
ロールは1本のまま、リードエンジニアだけにPassRoleを許可したくてsts:SourceIdentityを使った話

ログインアカウントからプロジェクトアカウントにAssumeRoleする構成で、切り替え先の共通ロールは1本のまま、リードエンジニア数名にだけ iam:PassRole を許可したくなりました。
sts:SourceIdentity で解決できたので備忘録兼同じことをする人に向けて残しておきます。地味にハマりどころが多かったです。

やりたかったこと  

前提はよくあるSSO未導入なAWSマルチアカウントの構成です。

  • ログインアカウント:IAMユーザーがいるログイン用アカウント。
  • プロジェクトアカウント:実際の作業先。ログイン側から共通ロール(ここでは developer_role とします)にAssumeRoleして入る。

この共通ロールは開発者全員が使います。で、そのうちリード数名にだけ特定の権限を追加で許可したい
(今回はECS RunTask実行時にECSタスクロールを渡すためのiam:PassRoleを追加付与したかった)
ロールは分けず共通ロール1本のまま、ユーザー単位で権限に差を付けたいという贅沢な要望を実現するのが目標です。
はいそこ、素直にロール分ければいいだろとか言わないでください。
大量のプロジェクトを共通の権限基盤で管理してるとこに個別対応入れるのめんd大変なんだから。
(あとユーザーグループの作成上限のハードリミット500に迫ってて余裕がない……)
というわけで悪あがきしたお話です。

やり方としては、拡張用のロールを別に切る・セッションタグ(ABAC)で admin/standard を属性管理する、といった選択肢もあるようですが、今回は対象がリード数名と少なく異動も頻繁じゃないので、プロジェクト側で名前でベタ書きするだけで管理できる SourceIdentity が一番お手軽シンプルなためこれで進めました。
(ログインアカウント側で個々のユーザーのタグまで管理するのがしんどいという本音もあったりしないこともない)

aws:username では縛れない  

真っ先に検討して撃沈したのが誰しも考えそうなシンプルにユーザー名で縛る方法です。
プロジェクト側で「呼んできたユーザー名」を条件にすればこんなの解決じゃんと思いますが、aws:username はIAMユーザーのときだけ有効で、AssumeRoleしたロールセッションでは空になり使えません

aws:userid のほうはロールセッションだと <ロールのユニークID>:<セッション名> になりますが、セッション名は呼び出し側が好きに付けられる=詐称し放題なので認可には使えません。(自分も普段は識別用に好き勝手設定してたりする)

つまり、AssumeRole先に渡せるパラメータで、元のユーザーが誰かを信頼できる形で使うには専用の仕組みが要ります。それが今回主役の SourceIdentity です。

設計:SourceIdentityで本人性を運ぶ  

aws:SourceIdentity は「このセッションの後ろにいる人間は誰か」を運ぶためのキーです。AssumeRole時に確定して、あとから変えられません。この変更不可という性質のおかげで、切り替え先でも認可や監査にも使えます。

引き受けたロールで実行されるアクションのモニタリングと制御 - AWS Identity and Access Management
AWS CloudTrail ログを使用して、ユーザーがロールを引き受けるときにソース ID を設定するよう要求することで、IAM ロールでアクションを実行したユーザーをモニタリングできます。
docs.aws.amazon.com

重要なのは 「ログイン側で、SourceIdentityがユーザー名と一致するときだけ許可する」 ことです。
ここを縛らないと、普通のユーザーが勝手に他人のSourceIdentityを名乗って昇格できてしまいます。
詐称されたらこの方式は終了なのでSourceIdentityを設定するならユーザー名一致強制は必須です。

いじるポリシーは3か所です。

1. ログインアカウント側(IAMユーザーのアイデンティティベース)  

AssumeRoleとSetSourceIdentityの両方をActionに入れて、SourceIdentityが自分のユーザー名と一致することを条件にします。

{
  "Effect": "Allow",
  "Action": [
    "sts:AssumeRole",
    "sts:SetSourceIdentity"
  ],
  "Resource": "arn:aws:iam::PROJECT_ACCOUNT_ID:role/developer_role",
  "Condition": {
    "StringLikeIfExists": {
      "sts:SourceIdentity": "${aws:username}"
    }
  }
}

ここは注意で、Actionsts:SetSourceIdentity を入れるのと、Conditionsts:SourceIdentity を書くのは別物です。Conditionはあくまで「許可されたActionの絞り込み」であって、それ単体でSetSourceIdentityというActionを許可してはくれません。両方書きます。

ちなみに演算子は StringLike(SourceIdentity必須)でも StringLikeIfExists(付けるなら一致必須/付けなくてもOK)でもいいです。前者は「必ず名乗らせる」、後者は「特権を使わないセッションはSourceIdentityなしでも通す」という設計になります。どちらにしても特権行使は後段のPassRole条件で弾かれるので、全員に設定強制しない場合は StringLikeIfExists にした方が追加付与したい権限を使わない人が巻き込まれないのでオヌヌメ。

2. プロジェクトアカウント側(信頼ポリシー)  

当たり前ですがsts:SetSourceIdentity を許可しておかないと、--source-identity 付きのAssumeRoleがそもそも弾かれます。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::LOGIN_ACCOUNT_ID:root"
      },
      "Action": [
        "sts:AssumeRole",
        "sts:SetSourceIdentity"
      ]
    }
  ]
}

3. プロジェクトアカウント側(権限ポリシー)  

ベース権限は全員に無条件、追加のPassRoleだけSourceIdentityで開けます。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Base",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "ec2:Describe*"
      ],
      "Resource": "*"
    },
    {
      "Sid": "PassRoleForLeadEngineers",
      "Effect": "Allow",
      "Action": "iam:PassRole",
      "Resource": "arn:aws:iam::PROJECT_ACCOUNT_ID:role/EcsTaskRole",
      "Condition": {
        "StringEquals": {
          "aws:SourceIdentity": [
            "alice",
            "bob"
          ],
          "iam:PassedToService": "ecs-tasks.amazonaws.com"
        }
      }
    }
  ]
}

PassRoleは権限昇格の入口になりやすいので、Resource* にしない(渡していいロールARNを列挙する)、iam:PassedToService で渡し先サービスを絞る、の2つはちゃんとやります。(この辺は今回の本題ではないので適宜制限してください)

AWS サービスにロールを渡すアクセス許可をユーザーに付与する - AWS Identity and Access Management
AWS サービスにロールを渡すアクセス許可を IAM ユーザーに付与する方法を学びます。
docs.aws.amazon.com

AWS_PROFILE運用のハマりどころ  

普段 AWS_PROFILE でプロファイルを切り替えて使っている人は、ここが一番のトラップになります。

~/.aws/config の普通のロール切り替え(role_arn + source_profile)には、SourceIdentityを渡す仕組みがありません
試しに source_identity = alice と書いてもエラーにならず、しれっと無視されました。
結果、SourceIdentityが空のままAssumeRoleされて、PassRole条件にマッチせず AccessDenied。しかも黙って無視されるだけなので特定にも時間がかかりました。

AWS CLIのドキュメントにそんなパラメータ見当たらないですが一応botocoreの実装も追ってみました。
が、 botocore/credentials.py を読むと、configから読んでいるのは role_session_name / external_id / mfa_serial / duration_seconds の4つだけで、source_identity はどこにも出てきません。ないものは当然読めません。

というわけで、プロファイル設定だけでSourceIdentityを乗せる方法は存在しません。面倒ですがcredential_process でパラメータをコネコネすることにしました。

credential_process の実装  

credential_process がやることは「Version:1 の決まった形式のJSONをstdoutに吐く」だけです。
AWS CLIの --query(JMESPath)でキー名を合わせてしまえば、整形用の別コマンドを挟まずCLI単体で完結します。

Process credential provider - AWS SDKs and Tools
SDKs provide a way to extend the credential provider chain for custom use cases. Configure this functionality by using the following settings. Relevant to the following SDKs and tools: AWS CLI,AWS SDK for C++,AWS SDK for Go,AWS SDK for Java,AWS SDK for JavaScript,AWS SDK for Kotlin,AWS SDK for .NET,AWS SDK for PHP,AWS SDK for Python (Boto3),AWS SDK for Ruby,AWS SDK for Rust,AWS SDK for Swift,AWS Tools for Windows PowerShell.
docs.aws.amazon.com

~/.aws/config

[profile project-alice]
credential_process = /usr/local/bin/assume-with-identity.sh alice arn:aws:iam::PROJECT_ACCOUNT_ID:role/developer_role
region = ap-northeast-1

assume-with-identity.sh

#!/usr/bin/env bash
set -euo pipefail

# Usage: assume-with-identity.sh <identity> <role-arn> [source-profile]
#   identity      : SourceIdentity と RoleSessionName に使う値(通常は IAM ユーザー名)
#   role-arn      : AssumeRole 先のロール ARN
#   source-profile: assume 元のプロファイル名(省略時は "default")

IDENTITY="${1:?usage: assume-with-identity.sh <identity> <role-arn> [source-profile]}"
ROLE_ARN="${2:?usage: assume-with-identity.sh <identity> <role-arn> [source-profile]}"
SOURCE_PROFILE="${3:-default}"

aws sts assume-role \
  --profile "$SOURCE_PROFILE" \
  --role-arn "$ROLE_ARN" \
  --role-session-name "$IDENTITY" \
  --source-identity "$IDENTITY" \
  --query '{Version: `1`, AccessKeyId: Credentials.AccessKeyId, SecretAccessKey: Credentials.SecretAccessKey, SessionToken: Credentials.SessionToken, Expiration: Credentials.Expiration}' \
  --output json

ミソは --query の中の `1` です。バッククォートで囲むとJMESPathのリテラル数値になって、Version が整数の 1 で出ます(credential_processはVersionを整数で欲しがります)。

出力がこの形になっていればOKです。

{
  "Version": 1,
  "AccessKeyId": "ASIA...",
  "SecretAccessKey": "...",
  "SessionToken": "...",
  "Expiration": "2026-07-15T12:34:56+00:00"
}

あとは AWS_PROFILE=project-alice に切り替えるだけで、勝手に --source-identity が乗ります。使う側の操作は今までどおりです。

SourceIdentityが乗っているか確認する方法  

CloudTrailからAssumeRole イベントを見ると、想定どおりのコンテキストが乗っているか事実確認できます。

  • userIdentity.type … 呼び出し主体がちゃんと IAMUser
  • requestParameters.sourceIdentity … SourceIdentityが実際に飛んでいるか

get-caller-identity にはSourceIdentityが出ないので、乗っているかの確認はCloudTrailで見るのが確実です。

セキュリティ・運用で意識していること  

最後に、この構成を組む上で注意すべき点をメモしておきます。

  • 本人性の担保はログイン側の sts:SourceIdentity == ${aws:username} 強制に全乗っかり。 このロールへのAssumeRole経路を増やさない前提なので複数経路から使用されるロールへの設定は慎重に行なってください。(ログインアカウントに絶対の統制が効いてる場合のみ使える手段です)
  • PassRoleなどのスコープはプロジェクト側の責任。 AssumeRole先で引き受けるロールはログインアカウントから手出し出来ないのでどんな権限を付与するかから全ての責任をプロジェクト側で負うことになります。
  • ユーザー名のリストはポリシー内ベタ書き配列。 数名なら管理もそこまで手間にはならないですが、退職・異動時の削除もプロジェクト側の責任で行うことになります。人数が増えたり中央管理に寄せられる場合はセッションタグ(ABAC)でadmin/standardを属性管理する方式への移行を検討した方がいいでしょう。(そもそもその時は複雑な制御入れずに素直にロール自体分割すべきだとも思うけど)

まとめ  

  • 切り替え先で「元のユーザーが誰か」を信頼できる形で使うなら sts:SourceIdentity。変更不可なので認可にも監査にも使える。
  • 本人性の担保は「ログイン側で sts:SourceIdentity == ${aws:username} を強制」がキモ。
  • .aws/config はSourceIdentityを運べない。credential_process を使う。整形は --query`1` リテラルでCLI単体で済む。
各種アカウント