クロスアカウントフェデレーションサインインするための URL を発行する Lambda 関数を作成してみる
いわさです。
AWS マネジメントコンソールはサインイントークンを使ったフェデレーションサインインを行う仕組みがあります。
Lambda 関数経由でこのサインイントークンを埋め込んだ URL を発行し、指定した AWS アカウントへサインインできる仕組みを作成してみました。
想定シナリオですが、まず踏み台となる AWS アカウントがあってそこで Lambda 関数が実行されています。
事前に別のターゲット AWS アカウントに IAM ロールを発行しておき、Lambda 実行ロールからの AssumeRole を許可しておきます。
Lambda 関数は指定した AWS アカウントに AssumeRole したうえで対象アカウント上のサインイントークンを払い出し、フェデレーションサインインを行うための URL を作成します。
図にするとこんな感じで考えています。
IAM ロールをデプロイ
Lambda 関数に必要な各種 IAM ロールを事前にデプロイしておきます。
この記事では Lambda 実行ロールをCrossAccountFederationLambdaRole
、各種ゲストアカウントに作成しておく AssumeRole 用 IAM ロールをCrossAccountTargetRole
という名前にしておきました。
Account A の Lambda 実行ロールはどの AWS アカウントに AssumeRole するかは未定ですが、IAM ロール名だけ固定化しておきます。
CloudFormation テンプレートで作っておきました。
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
マネージドポリシーを割り当てておきますが、ここは必要なポリシー権限で見直してもらうということで。
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 アカウントとセッション名を指定できるようにだけしておきます。
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 を固定で割り当てたりしています。
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 が生成されました。良さそう。
シークレットブラウザで上記 URL にアクセスしてみましょう。
指定した AWS アカウントに指定したセッション名でサインインすることが出来ました。
セッション上限は 1 時間
なお、本記事の実装方法の場合セッション時間は 1 時間となります。
IAM ロールには最大セッション時間の概念があり最大 12 時間指定可能で、AssumeRole 実行時にはセッションの有効期間をその上限範囲で指定することが可能です。
ただし、今回はロールからロールに AssumeRole する「ロールチェーン」と呼ばれる方式になり、1 時間を超えることが出来ないという制限事項があります。
また、IAMロールから異なるIAMロールへスイッチする(チェインする)ことも可能ですが、この場合には1hを超えるSessionDuration指定はできません。
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時間
})
);
:
さいごに
本日はクロスアカウントフェデレーションサインインするための URL を発行する Lambda 関数を作成してみました。
実際にクロスアカウントでのフェデレーションサインインを実現することはできました。
しかし、最大セッション時間が 1 時間までという制限事項から採用できないケースも多そうです。
別途クロスアカウントの長時間セッションが必要な場合の代替案を検討してみます。