Node.js AWS SDKでSTS AssumeRoleを行い安全にクロスアカウントアクセスする
概要
お客さんによっては、自社のデータを他社のサービス上にアップロードすることを躊躇うことがある。その場合、お客さんのAWSリソースにアクセスしてデータを参照することになるが、STSのAssumeRoleを利用するのが安全でセキュアなやり方のようだったので、その理由や方法を備忘として残しておく。
AssumeRoleとは
詳しくはクラスメソッドさんのこちらの記事を読んでもらえば分かるので割愛するが、Roleを引き受けるということ。
Roleを引き受けると、引き受けた側はそのRoleが持つ権限を使えるようになる。例えば、お客さんのアカウントで作られたRoleを、あるSaaSサービスが引き受けると、お客さんの代わりにそのRole権限の範囲でお客さんのAWSリソースにアクセスすることができる。
なぜAssumeRoleなのか
AssumeRoleの場合、お客さんが提供する情報は Role ARN だけとなり、管理がしやすいというメリットがかなり大きい。
また、外部IDという Role ARN が漏洩した場合の対策も存在し、よりセキュアなことも他の方法との違いだ。外部IDによるセキュリティの強化に関してはこちらの記事が分かりやすい。
S3バケットポリシーは?
お客さんのS3バケットにアクセスするケースを考えてみる。
AssumeRoleの代わりに、バケットポリシーでお客さんに自社アカウントからのアクセスを許可してもらっても、同じようにお客さんのデータにアクセスできる。
しかし、お客さんからしたら、どのバケットにどんな設定をしたのか、アクセスを許可するバケットが増えるほど管理が難しくなる。
その点AssumeRoleでは、IAMで分かりやすい名前をつけたり、他の権限と一括管理ができるので、管理が非常に簡単になる。原則、権限はIAMで管理するのが最も安全なのだ。
アクセスキーは?
任意の権限を持つアクセスキー・シークレットキーを別サービスに使ってもらう方法はどうだろうか。
「作った当初は、XXX権限をもったアクセスキーをYYYというサービスに登録している」と覚えておくことができるかもしれないが、半年もするとわからなくなる可能性が高い。つまり忘れないようにする仕組みがIAMの他に別途必要ということになる。
また、キーが漏洩した場合は悪用されやすいのでその点にも不安が残る。
実装例
自社アカウントが、お客さんアカウントのS3バケットにアクセスするケースでAssumeRoleする実装例を紹介する(TypeScriptで実装)。
前提
- 自社サービスが、お客さん用に外部IDを発行している
- お客さんアカウントで、上記外部IDを使用して、自社アカウントを信頼、かつS3アクセスを部分的に許可するPolicyを持つRoleを作成している
設定してもらうPolicyはこんな感じ。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::{自社アカウントID}:root"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "{外部ID}"
}
}
}
]
}
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "S3Access",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:ListBucketVersions",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::{バケット名}/*",
"arn:aws:s3:::{バケット名}"
]
}
]
}
自社サービスがお客さんのRoleをAssumeRoleした上でS3クライアントを取得し、getObjectするソースコードはこんな感じ。
import AWS from "aws-sdk";
const main = async () => {
const s3 = await getS3ClientByAssumeRole(
"{RoleARN}",
"{外部ID}",
);
if (s3) {
// TODO: create params
s3.getObject(params, (err, data) => {
...
});
} else {
...
}
};
const getS3ClientByAssumeRole = async (
roleARN: string,
externalId: string,
): Promise<AWS.S3 | undefined> => {
const creds = await getCredentials(roleARN, externalId);
if (!creds) return undefined;
return new AWS.S3({
apiVersion: "2006-03-01",
accessKeyId: creds.AccessKeyId,
secretAccessKey: creds.SecretAccessKey,
sessionToken: creds.SessionToken,
});
}
const getCredentials = (roleARN: string, externalId: string): Promise<AWS.STS.Credentials | undefined> => {
const sts = new AWS.STS({
apiVersion: "2011-06-15",
});
return new Promise((resolve, reject) => {
const params: AWS.STS.AssumeRoleRequest = {
RoleArn: roleARN,
ExternalId: externalId,
RoleSessionName: new Date().getTime().toString(),
};
sts.assumeRole(params, (err, data) => {
if (err) reject(err);
resolve(data?.Credentials);
});
});
};
main();
Discussion