クロスアカウントフェデレーションサインインするための URL を発行する Lambda 関数を作成してみる

クロスアカウントフェデレーションサインインするための URL を発行する Lambda 関数を作成してみる

Clock Icon2025.02.04

いわさです。

AWS マネジメントコンソールはサインイントークンを使ったフェデレーションサインインを行う仕組みがあります。

https://dev.classmethod.jp/articles/its-not-switch-role-its-assumerole/

Lambda 関数経由でこのサインイントークンを埋め込んだ URL を発行し、指定した AWS アカウントへサインインできる仕組みを作成してみました。
想定シナリオですが、まず踏み台となる AWS アカウントがあってそこで Lambda 関数が実行されています。
事前に別のターゲット AWS アカウントに IAM ロールを発行しておき、Lambda 実行ロールからの AssumeRole を許可しておきます。
Lambda 関数は指定した AWS アカウントに AssumeRole したうえで対象アカウント上のサインイントークンを払い出し、フェデレーションサインインを行うための URL を作成します。

図にするとこんな感じで考えています。

317A6E21-5486-47D9-8515-EBF063D36767.png

IAM ロールをデプロイ

Lambda 関数に必要な各種 IAM ロールを事前にデプロイしておきます。
この記事では Lambda 実行ロールをCrossAccountFederationLambdaRole、各種ゲストアカウントに作成しておく AssumeRole 用 IAM ロールをCrossAccountTargetRoleという名前にしておきました。

Account A の Lambda 実行ロールはどの AWS アカウントに AssumeRole するかは未定ですが、IAM ロール名だけ固定化しておきます。
CloudFormation テンプレートで作っておきました。

account-a.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: '---'
Resources:
  LambdaExecutionRole:
    Type: 'AWS::IAM::Role'
    Properties:
      RoleName: 'CrossAccountFederationLambdaRole'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: 'lambda.amazonaws.com'
            Action: 'sts:AssumeRole'
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'
      Policies:
        - PolicyName: 'AssumeTargetRolePolicy'
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action: 'sts:AssumeRole'
                Resource: 'arn:aws:iam::*:role/CrossAccountTargetRole'
Outputs:
  LambdaExecutionRoleArn:
    Value: !GetAtt LambdaExecutionRole.Arn

続いて、Account B・Account C で用意しておく IAM ロールです。
Account A の Lambda 関数からのみ AssumeRole を許可します。
Assume 後の権限は一旦はReadOnlyAccessマネージドポリシーを割り当てておきますが、ここは必要なポリシー権限で見直してもらうということで。

account-b.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: '---'
Parameters:
  AccountAId:
    Type: String
  LambdaRoleName:
    Type: String
    Default: 'CrossAccountFederationLambdaRole'
Resources:
  CrossAccountTargetRole:
    Type: 'AWS::IAM::Role'
    Properties:
      RoleName: 'CrossAccountTargetRole'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              AWS: !Sub 'arn:aws:iam::${AccountAId}:role/${LambdaRoleName}'
            Action: 'sts:AssumeRole'
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/ReadOnlyAccess'
Outputs:
  CrossAccountTargetRoleArn:
    Value: !GetAtt CrossAccountTargetRole.Arn

上記テンプレートを使ってスタックを各アカウントに作成しておきました。
CloudFormation テンプレートをデプロイする手順はこの記事では割愛します。

Lambda 関数を作成

諸事情により今回は TypeScript (Node.js 20.x) で関数を作成したいと思います。
ビルドとデプロイが楽なので SAM CLI を使います。

% sam init -r nodejs20.x 
Which template source would you like to use?
        1 - AWS Quick Start Templates
        2 - Custom Template Location
Choice: 1

Choose an AWS Quick Start application template
        1 - Hello World Example
        2 - GraphQLApi Hello World Example
        3 - Hello World Example with Powertools for AWS Lambda
        4 - Multi-step workflow
        5 - Standalone function
        6 - Scheduled task
        7 - Data processing
        8 - Serverless API
        9 - Full Stack
        10 - Lambda Response Streaming
Template: 1

Based on your selections, the only Package type available is Zip.
We will proceed to selecting the Package type as Zip.

Based on your selections, the only dependency manager available is npm.
We will proceed copying the template using npm.

Select your starter template
        1 - Hello World Example
        2 - Hello World Example TypeScript
Template: 2

:

関数コードは次のような感じにしました。トライアンドエラーで色々修正していますが Claude 3.5 Sonnet v2 に作成支援をしてもらいました。いつもありがとう。
インターフェースとしては Lambda 実行時のインプットでターゲット AWS アカウントとセッション名を指定できるようにだけしておきます。

hoge0202lambda/hello-world/app.ts
import { Handler } from 'aws-lambda';
import { 
  STSClient, 
  AssumeRoleCommand
} from '@aws-sdk/client-sts';

const ROLE_NAME = 'CrossAccountTargetRole';
const FEDERATION_ENDPOINT = 'https://signin.aws.amazon.com/federation';
const CONSOLE_URL = 'https://console.aws.amazon.com/';

interface EventInput {
  targetAccountId: string;
  roleSessionName: string;
}

export const handler: Handler = async (event: EventInput) => {
  try {
    if (!event.targetAccountId || !event.roleSessionName) {
      return {
        statusCode: 400,
        body: JSON.stringify({
          message: 'targetAccountId and roleSessionName are required'
        })
      };
    }

    //AssumeRole
    const roleArn = `arn:aws:iam::${event.targetAccountId}:role/${ROLE_NAME}`;
    const stsClient = new STSClient({ region: 'ap-northeast-1' });
    const assumeRoleResponse = await stsClient.send(
      new AssumeRoleCommand({
        RoleArn: roleArn,
        RoleSessionName: event.roleSessionName,
        DurationSeconds: 3600,
      })
    );
    if (!assumeRoleResponse.Credentials) {
      throw new Error('Failed to assume role');
    }

    //federatedSigninUrl
    const credentials = {
      sessionId: assumeRoleResponse.Credentials.AccessKeyId,
      sessionKey: assumeRoleResponse.Credentials.SecretAccessKey,
      sessionToken: assumeRoleResponse.Credentials.SessionToken
    };
    const signinTokenResponse = await fetch(
      `${FEDERATION_ENDPOINT}?${new URLSearchParams({
        Action: 'getSigninToken',
        SessionType: 'json',
        Session: JSON.stringify(credentials)
      }).toString()}`
    );
    const signinToken = await signinTokenResponse.json();
    const loginParams = new URLSearchParams({
      Action: 'login',
      Destination: CONSOLE_URL,
      SigninToken: signinToken.SigninToken
    });
    const federatedSigninUrl = `${FEDERATION_ENDPOINT}?${loginParams.toString()}`;

    return {
      statusCode: 200,
      body: JSON.stringify({
        signInUrl: federatedSigninUrl
      })
    };
  } catch (error) {
    console.error('Error:', error);
    return {
      statusCode: 500,
      body: JSON.stringify({
        message: 'Internal server error'
      })
    };
  }
};

SAM テンプレート側は次のように修正しています。
API イベントを削除したり、関数実行 ロールに既存 ARN を固定で割り当てたりしています。

hoge0202lambda/tempalte.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: ---
Globals:
  Function:
    Timeout: 3

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello-world/
      Handler: app.handler
      Runtime: nodejs20.x
      Architectures:
        - x86_64
      Role: arn:aws:iam::123456789012:role/CrossAccountFederationLambdaRole
    Metadata:
      BuildMethod: esbuild
      BuildProperties:
        Minify: true
        Target: "es2020"
        Sourcemap: true
        EntryPoints: 
        - app.ts

sam buildしてsam deployしましょう。
ここの手順も割愛します。

実行してサインインしてみる

今回は Lambda コンソール上からインプットパラメータを指定して実行してみます。
パラメータは次のような感じです。targetAccountIdは冒頭の構成図でいう Account B / Account C です。
roleSessionNameは識別可能な任意の文字列で。

{
  "targetAccountId": "111122223333",
  "roleSessionName": "hogeiwasa1"
}

実行してみると、サインイントークンを含んだ URL が生成されました。良さそう。

BA8ED3F3-8B1C-445F-A45B-7CD1C8C755A8.png

シークレットブラウザで上記 URL にアクセスしてみましょう。
指定した AWS アカウントに指定したセッション名でサインインすることが出来ました。

5722FB7D-A010-4AD5-8918-EA34853A00A6.png

セッション上限は 1 時間

なお、本記事の実装方法の場合セッション時間は 1 時間となります。
IAM ロールには最大セッション時間の概念があり最大 12 時間指定可能で、AssumeRole 実行時にはセッションの有効期間をその上限範囲で指定することが可能です。

ただし、今回はロールからロールに AssumeRole する「ロールチェーン」と呼ばれる方式になり、1 時間を超えることが出来ないという制限事項があります。

また、IAMロールから異なるIAMロールへスイッチする(チェインする)ことも可能ですが、この場合には1hを超えるSessionDuration指定はできません。

https://dev.classmethod.jp/articles/understanding-iam-role-and-switch-role/

IAM ロールの最大セッション時間に 12 時間の設定は可能ですが、Lambda 関数上で AssumeRole する際に 1 時間を超えて指定してみたところ「The requested DurationSeconds exceeds the 1 hour session limit for roles assumed by role chaining」エラーが発生しました。

:
    // Account BのRoleにAssume Role
    const assumeRoleResponse = await stsClient.send(
      new AssumeRoleCommand({
        RoleArn: roleArn,
        RoleSessionName: event.roleSessionName,
        DurationSeconds: 43200, // 12時間
      })
    );
:

783BA2E9-9251-4567-A34D-1FA45661F98C.png

さいごに

本日はクロスアカウントフェデレーションサインインするための URL を発行する Lambda 関数を作成してみました。

実際にクロスアカウントでのフェデレーションサインインを実現することはできました。
しかし、最大セッション時間が 1 時間までという制限事項から採用できないケースも多そうです。

別途クロスアカウントの長時間セッションが必要な場合の代替案を検討してみます。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.