いわさです。
先日 myApplication を使って同一 AWS アカウント内で複数サービスを運用している際に、サービスごとの管理がしやすくなるかを確認しました。
本日はその延長というか、同じような混在環境で、あるサービスの担当者が別のサービスにアクセスしてしまっていないかを確認する仕組みを考えたいと思います。
方針
AWS re:Invent 2023 で私はマルチテナント SaaS のワークショップに参加しまして、そこではあるテナントのユーザーが別のテナントのデータへのアクセス違反を行っていないか監査する仕組みを CloudTrail と Athena で実装していました。
このワークショップを実施した際に「これはサービス分離にも応用できそうだな」と思いました。
ワークショップでは STS のログと、DynamoDB のキーからテナント ID の突合を行っているのですが、今回はそもそもリソースごとにサービスを関連づける必要があるということで、ユーザーやリソースにサービスを識別するためのタグを設定して CloudTrail イベントと突合出来る状態なのかを確認してみることにしました。
確認した結果タグ情報が出力されないことがわかったので、まずは Step Functions を使ってユーザーやリソースのタグ情報を S3 バケットに出力するところまで試してみました。
CloudTrail へはタグは出力されない
まずは次のように IAM ユーザーと S3 バケット、オブジェクトへタグを設定し、CloudTrail にどのような内容が出力されるのかを確認してみました。
% aws iam list-user-tags --user-name hoge0108 --profile hogeadmin
{
"Tags": [
{
"Key": "usertag",
"Value": "hoge0108"
}
]
}
% aws s3api get-bucket-tagging --bucket hoge0108 --profile hogeadmin
{
"TagSet": [
{
"Key": "buckettag",
"Value": "hoge0108"
}
]
}
% aws s3api get-object-tagging --bucket hoge0108 --key hoge.txt --profile hogeadmin
{
"TagSet": [
{
"Key": "bucketobjecttag",
"Value": "hoge0108"
}
]
}
前提として証跡のデータイベントを有効化しています。
確認した結果、CloudTrail ログは次のような出力内容となっており、タグ情報は確認が出来ませんでした。
確認ポイントとしてはuserIdentity
、あとはresources
あたりですね。
CreateTag など、タグ設定系の API ではタグが出力されていたので、もしかしたらと思ったのですが、ダメでした。
{
"eventVersion": "1.09",
"userIdentity": {
"type": "IAMUser",
"principalId": "AKIAIOSFODNN7EXAMPLE",
"arn": "arn:aws:iam::123456789012:user/hoge0108",
"accountId": "123456789012",
"accessKeyId": "AKIAIOSFODNN7EXAMPLE",
"userName": "hoge0108"
},
"eventTime": "2024-01-07T22:35:30Z",
"eventSource": "s3.amazonaws.com",
"eventName": "GetObject",
"awsRegion": "ap-northeast-1",
"sourceIPAddress": "203.0.113.1",
"userAgent": "[aws-cli/2.13.37 Python/3.11.6 Darwin/22.6.0 exe/x86_64 prompt/off command/s3.cp]",
"requestParameters": {
"bucketName": "hoge0108",
"Host": "hoge0108.s3.ap-northeast-1.amazonaws.com",
"key": "hoge.txt"
},
"responseElements": null,
"additionalEventData": {
"SignatureVersion": "SigV4",
"CipherSuite": "ECDHE-RSA-AES128-GCM-SHA256",
"bytesTransferredIn": 0,
"AuthenticationMethod": "AuthHeader",
"x-amz-id-2": "vPiEwMys9PYOOFgDufR4JJWVkwj9yexEjDOjBrdWG2Nj/4vF9Qi4PdnB4bMGgOZdxlQhTan3S4jAmheDRCWaBA==",
"bytesTransferredOut": 6
},
"requestID": "PTX8RDTWMH8Z59HA",
"eventID": "dd9d1db6-689f-4161-bae7-d0292471c19e",
"readOnly": true,
"resources": [
{
"type": "AWS::S3::Object",
"ARN": "arn:aws:s3:::hoge0108/hoge.txt"
},
{
"accountId": "123456789012",
"type": "AWS::S3::Bucket",
"ARN": "arn:aws:s3:::hoge0108"
}
],
"eventType": "AwsApiCall",
"managementEvent": false,
"recipientAccountId": "123456789012",
"eventCategory": "Data",
"tlsDetails": {
"tlsVersion": "TLSv1.2",
"cipherSuite": "ECDHE-RSA-AES128-GCM-SHA256",
"clientProvidedHostHeader": "hoge0108.s3.ap-northeast-1.amazonaws.com"
}
}
私が知る限りは CloudTrail の出力内容をカスタマイズは出来ない認識なので、別の方法でタグ情報を収集し、Athena などで分析結果を作成する際に結合する形になりそうだなと思いました。
タグを出力する仕組みを作成する
そこでまずは AWS の API でタグ情報が出力出来るのかを確認します。
まぁそれぞれのサービスで対象リソースを出力する仕組みはあるのですが、出来れば共通で一括出力したいところです。
AWS CLI のリファレンスを眺めてみるとaws resourcegroupstaggingapi
のget-resources
という API を使うとリソースとタグを一式取得出来そうということがわかりました。
試してみたところ大量のリソース ARN とタグがリストアップされます。
また、指定したタグキーが付与されたリソースのみをリストアップすることも出来ます。これは良さそうです。
% aws resourcegroupstaggingapi get-resources --tag-filters Key=hoge0108 --profile hogeadmin
{
"ResourceTagMappingList": [
{
"ResourceARN": "arn:aws:s3:::hoge0108",
"Tags": [
{
"Key": "hoge0108",
"Value": "aaa"
}
]
},
{
"ResourceARN": "arn:aws:s3:::hoge0108b",
"Tags": [
{
"Key": "hoge0108",
"Value": "bbb"
}
]
}
]
}
聞いたことが無かった API だったのと、ユースケースがかなり限られそうなのであまり使う人いないのかなと思って DevIO を調べてみると、いました。使ってる人が。
こちらを参考にさせていただくと、どうやらすべてのリソース情報が出力されるわけではないとのことで注意が必要そうです。
また、今回のポイントである IAM も対象外リソースとなっており、こちらは別の方法で取得する必要がありそうです。
確認してみると、ユーザーやロールをリストアップする API はそれぞれ存在しているのですが出力項目にタグは含まれていないようです。
それとは別で指定したユーザーやロールのタグをリストアップする API は用意されていました。
- list-user-tags — AWS CLI 1.32.14 Command Reference
- list-role-tags — AWS CLI 1.32.14 Command Reference
これを組み合わせて頑張ってみることにしました。
Step Functions で実装
色々なやり方があるのかなと思ったのですが、極力コードは書きたくないので Step Functions で実装してみることにしました。
リソースのタグ一式の取得は先程のresourcegroupstaggingapi
の API を使ってその結果を S3 に出力してみます。
ユーザーのタグ一式の取得はlist-users
とlist-user-tags
を組み合わせてみます。
運用を考えるとロールも取得するべきなのですが、今回は検証目的なので割愛します。
リソースとタグを列挙する
こちらはgetResources
を実行して S3 に出力しているだけです。
もしかしたら Athena などで取り扱うにあたって加工したほうが良いのかもしれないのですが、まぁ今回は気にしないでおきます。
CloudTrail ログが時系列データに対して、この出力しているタグ&リソース一覧はある特定時点の状態を出力したものです。そのあたりも考慮が必要ですね。
{
"Comment": "hogehoge",
"StartAt": "GetResources",
"States": {
"GetResources": {
"Type": "Task",
"Next": "PutObject",
"Parameters": {},
"Resource": "arn:aws:states:::aws-sdk:resourcegroupstaggingapi:getResources"
},
"PutObject": {
"Type": "Task",
"End": true,
"Parameters": {
"Body.$": "$.ResourceTagMappingList",
"Bucket": "hogeresourcelist",
"Key": "MyData",
"ContentType": "application/json"
},
"Resource": "arn:aws:states:::aws-sdk:s3:putObject"
}
}
}
リソース数がかなり多くなる場合があると思います、結果のページング仕様についても考えておくべきですね。それも今回は無視してます。
あとは、チバユキさんの記事に記載のあったように対象リソースが許容範囲か確認しておいたほうが良いですね。
S3 バケットはサポートされていましたが、オブジェクトについてはサポートされていないようでした。
ユーザーとタグを列挙する
ユーザーについては Map を使ってユーザーごとにタグを呼び出す形にしています。
これは実行時間がかかりそうなのと、API のレートが大丈夫かなってのが気にしておく点でしょうか。
リトライ処理を入れたり Wait を挟めばどうにかなりそうな気もしています。
{
"Comment": "fugafuga",
"StartAt": "ListUsers",
"States": {
"ListUsers": {
"Type": "Task",
"Parameters": {},
"Resource": "arn:aws:states:::aws-sdk:iam:listUsers",
"Next": "Map"
},
"Map": {
"Type": "Map",
"ItemProcessor": {
"ProcessorConfig": {
"Mode": "INLINE"
},
"StartAt": "ListUserTags",
"States": {
"ListUserTags": {
"Type": "Task",
"Parameters": {
"UserName.$": "$.UserName"
},
"Resource": "arn:aws:states:::aws-sdk:iam:listUserTags",
"ResultPath": "$.Tags",
"Next": "Convert"
},
"Convert": {
"Type": "Pass",
"End": true,
"Parameters": {
"UserName.$": "$.UserName",
"HogeTag.$": "$.Tags"
}
}
}
},
"InputPath": "$.Users",
"Next": "PutObject"
},
"PutObject": {
"Type": "Task",
"End": true,
"Parameters": {
"Body.$": "$",
"Bucket": "hoge0108",
"Key": "MyData",
"ContentType": "application/json"
},
"Resource": "arn:aws:states:::aws-sdk:s3:putObject"
}
}
}
これらから次のようにファイルを出力することが出来ました。
次はユーザーの一覧です。
[
{
"HogeTag": {
"IsTruncated": false,
"Tags": [
{
"Key": "hoge0108",
"Value": "aaa"
}
]
},
"UserName": "hoge0108"
},
{
"HogeTag": {
"IsTruncated": false,
"Tags": []
},
"UserName": "hogeadmin"
},
{
"HogeTag": {
"IsTruncated": false,
"Tags": []
},
"UserName": "iwasa"
}
]
次はリソースの一覧です。
[
:
{
"ResourceARN": "arn:aws:s3:::hoge0108",
"Tags": [
{
"Key": "hoge0108",
"Value": "aaa"
}
]
},
:
{
"ResourceARN": "arn:aws:sagemaker:ap-northeast-1:123456789012:experiment-trial-component/canvas1697422261165-cnnqr-train-backtest-1-1-dd98a9130f394ef289-aws-training-job",
"Tags": [
{
"Key": "sagemaker:user-profile-arn",
"Value": "arn:aws:sagemaker:ap-northeast-1:123456789012:user-profile/d-ddafwgprdn23/default-20231016t093667"
},
{
"Key": "sagemaker:domain-arn",
"Value": "arn:aws:sagemaker:ap-northeast-1:123456789012:domain/d-ddafwgprdn23"
},
{
"Key": "CanvasModelName",
"Value": "New model 2023-10-16 10:18 AM"
},
{
"Key": "Source",
"Value": "SageMakerCanvas"
}
]
}
]
文中に色々考慮事項を補足していますが、あとはこれと CloudTrail ログを組み合わせればなんとかなりそうな気がしてきました。
さいごに
本日は CloudTrail ログにプリンシパルやリソースのタグが出力されなかったので、ユーザーとリソースのタグ一覧を Step Functions で作成してみました。
次回は実際に監査が出来るようにログの分析を行い、サービスアクセスの違反が起きたかどうかを確認してみたいと思います。
「実はこうやったらもっとよく出来るぞ...」みたいな情報があれば、ください。