こんにちは、広野です。
以下の記事で、Storage Browser for Amazon S3 の実装方法を紹介しました。
また、追加記事でダウンロードを禁止する設定にしました。
しかしながら、このままではセキュリティがザルなので、実用的ではありません。本記事では、アクセスログを追加してみたのでその方法を紹介します。
追加した設定
- Amazon S3 のアクセスログ取得
設計上、Amazon Cognito のユーザーで認証しています。ですので、Amazon Cognito ユーザー名を含めたログを残します。
Storage Browser を使用するためのアーキテクチャは以下の記事と同じです。Amazon Cognito ユーザープールの認証で React アプリにログインし、ログインしたユーザーに Cognito ID プールで一時的な IAM ロールを割り当てます。付与された権限で Amazon S3 バケットを読み書きします。
Storage Browser で読み書きする Amazon S3 バケットへのアクセスログを残します。
検索しやすいように、今回は AWS CloudTrail Lake イベントデータストアを使用します。該当の Amazon S3 バケットのデータイベントのみを保存するようにしました。
基本的には公式の手順通りなのですが、該当の Amazon S3 バケットのデータイベントのみを保存する設定が必要です。マネジメントコンソールでは、以下のように設定しました。気を付けないといけないのは、Value の欄に S3 バケットの ARN を入れるのですが、バケット名の最後に / (スラッシュ) を入れないようにしましょう。
Amazon Cognito Lake イベントデータストアは Amazon Athena と連携して Athena からクエリできるようにもできるのですが、今回はやめました。他のデータソースと統合したければ、Athena でクエリするようにした方が良いです。
AWS CloudFormation でデプロイしたので、テンプレートも紹介しておきます。366 日で削除する S3 ライフサイクル設定を入れています。
AWSTemplateFormatVersion: 2010-09-09 Description: The CloudFormation template that creates a S3 bucket with CloudTrail logs for Storage Browser for Amazon S3. # ------------------------------------------------------------# # Input Parameters # ------------------------------------------------------------# Parameters: SubName: Type: String Description: Unique system sub name of example. (e.g. dev) Default: dev MaxLength: 10 MinLength: 1 DomainName: Type: String Description: Domain name of the Storage Browser URL. xxxxx.xxx (e.g. sampledomain.name) Default: sampledomain.name MaxLength: 40 MinLength: 5 AllowedPattern: "[^\\s@]+\\.[^\\s@]+" SubDomainName: Type: String Description: Sub domain name for URL. xxxxx.sampledomain.name (e.g. dev) Default: dev MaxLength: 20 MinLength: 1 S3DataEventRetentionDays: Type: Number Description: The retention period (days) for S3 data event logs. Enter an integer between 90 to 540. Default: 366 MaxValue: 540 MinValue: 90 Resources: # ------------------------------------------------------------# # S3 # ------------------------------------------------------------# S3Bucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub example-${SubName}-storagebrowser PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true CorsConfiguration: CorsRules: - AllowedHeaders: - "*" AllowedMethods: - "GET" - "HEAD" - "PUT" - "POST" - "DELETE" AllowedOrigins: - !Sub https://${SubDomainName}.${DomainName} ExposedHeaders: - last-modified - content-type - content-length - etag - x-amz-version-id - x-amz-request-id - x-amz-id-2 - x-amz-cf-id - x-amz-storage-class - date - access-control-expose-headers MaxAge: 3000 Tags: - Key: Cost Value: !Sub example-${SubName} # ------------------------------------------------------------# # CloudTrail Event Data Store for S3 kbdatasource # ------------------------------------------------------------# CloudTrailEventDataStore: Type: AWS::CloudTrail::EventDataStore Properties: Name: !Sub ${S3Bucket}-s3-data-events BillingMode: EXTENDABLE_RETENTION_PRICING FederationEnabled: false IngestionEnabled: true MultiRegionEnabled: false OrganizationEnabled: false RetentionPeriod: !Ref S3DataEventRetentionDays TerminationProtectionEnabled: true AdvancedEventSelectors: - Name: !Sub ${S3Bucket}-s3-data-events FieldSelectors: - Field: eventCategory Equals: - Data - Field: "resources.type" Equals: - AWS::S3::Object - Field: resources.ARN StartsWith: - !Sub "arn:aws:s3:::${S3Bucket}" Tags: - Key: Cost Value: !Sub example-${SubName} DependsOn: - S3Bucket
ログの検索
ログは AWS CloudTrail Lake データストアに保存されていますので、SQL で検索することができます。しかし、AWS CloudTrail のデータイベントログを 1 件出力すると、以下のような階層構造を含むデータが記録されます。
これでは、どこに何があるのか見つけられません。
[ { "eventVersion": "1.11", "userIdentity": "{type=AssumedRole, principalid=AROATLXY4HCCXNO6SVHIB:CognitoIdentityCredentials, arn=arn:aws:sts::999999999999:assumed-role/xxxx-CognitoIdPAuthRole-scsk/CognitoIdentityCredentials, accountid=999999999999, accesskeyid=ASIATLXY4HCCQTLMWCZV, username=null, sessioncontext={attributes={creationdate=2024-12-12 05:22:57.000, mfaauthenticated=false}, sessionissuer={type=Role, principalid=AROATLXY4HCCXNO6SVHIB, arn=arn:aws:iam::999999999999:role/xxxx-CognitoIdPAuthRole-scsk, accountid=999999999999, username=xxxx-CognitoIdPAuthRole-scsk}, webidfederationdata={federatedprovider=cognito-identity.amazonaws.com, attributes={cognito-identity.amazonaws.com:amr=[\"authenticated\",\"cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_BukHorAF0\",\"cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_BukHorAF0:CognitoSignIn:c7243a48-1081-70e9-43cf-b90864365a29\"], cognito-identity.amazonaws.com:aud=ap-northeast-1:2f961a08-67b6-4918-9711-09317abeb02b, cognito-identity.amazonaws.com:sub=ap-northeast-1:35df1c38-8506-c926-5a2e-1b0159e0da8d}}, sourceidentity=null, ec2roledelivery=null, ec2issuedinvpc=null, assumedroot=null}, invokedby=null, identityprovider=null, credentialid=null, onbehalfof=null, inscopeof=null}", "eventTime": "2024-12-12 05:23:20.000", "eventSource": "s3.amazonaws.com", "eventName": "PutObject", "awsRegion": "ap-northeast-1", "sourceIPAddress": "xxx.xxx.131.235", "userAgent": "[Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0]", "errorCode": "", "errorMessage": "", "requestParameters": "{If-None-Match=*, bucketName=xxxx-scsk-storagebrowser, Host=xxxx-scsk-storagebrowser.s3.ap-northeast-1.amazonaws.com, key=bot2/test6.txt}", "responseElements": "{x-amz-server-side-encryption=AES256}", "additionalEventData": "{SignatureVersion=SigV4, CipherSuite=TLS_AES_128_GCM_SHA256, bytesTransferredIn=5, SSEApplied=Default_SSE_S3, AuthenticationMethod=AuthHeader, x-amz-id-2=H+RhWUuEaBNn5NzXQZzfPmQNhcf9jyTosjNHd+PPrJ9xTTp0CVlR+fvU/t8EJpsKPj/1AxZ+osc=, bytesTransferredOut=0}", "requestID": "NFYGVKXT5894ZXYJ", "eventID": "f9e3aedf-7a2b-4359-9c6d-7291a8a97bd9", "readOnly": "false", "resources": "[{accountid=null, type=AWS::S3::Object, arn=arn:aws:s3:::xxxx-scsk-storagebrowser/bot2/test6.txt, arnprefix=null}, {accountid=999999999999, type=AWS::S3::Bucket, arn=arn:aws:s3:::xxxx-scsk-storagebrowser, arnprefix=null}]", "eventType": "AwsApiCall", "apiVersion": "", "managementEvent": "false", "recipientAccountId": "999999999999", "sharedEventID": "", "annotation": "", "vpcEndpointId": "", "vpcEndpointAccountId": "", "serviceEventDetails": "", "addendum": "", "edgeDeviceDetails": "", "insightDetails": "", "eventCategory": "Data", "tlsDetails": "{tlsversion=TLSv1.3, ciphersuite=TLS_AES_128_GCM_SHA256, clientprovidedhostheader=xxxx-scsk-storagebrowser.s3.ap-northeast-1.amazonaws.com}", "sessionCredentialFromConsole": "" } ]
ですので、ログを以下の項目に整形して表示したいと思います。
eventTime | 日時 |
eventName | オペレーションの種類 |
cognitoUser | Cognito ユーザー名 |
bucketName | S3 バケット名 |
key | S3 オブジェクトキー名 |
sourceIpAddress | ソース IP アドレス |
userAgent | デバイス情報 |
errorCode | エラーコード |
errorMessage | エラーメッセージ |
AWS CloudTrail Lake イベントデータストアに対して、以下のクエリを実行します。
FROM で指定するテーブル名は、画面でイベントデータストアを選択すると自動で ID が入ります。
WHERE 以下の条件は、適宜変更してください。
SELECT eventTime, eventName, split_part(json_extract_scalar(userIdentity.sessioncontext.webidfederationdata.attributes['cognito-identity.amazonaws.com:amr'],'$[2]'),':',CAST(3 AS BIGINT)) AS cognitoUser, element_at(requestParameters,'bucketName') AS bucketName, element_at(requestParameters,'key') AS key, sourceIpAddress, userAgent, errorCode, errorMessage FROM 137d78ec-d8ac-4dcf-aebb-xxxxxxxxxxxxx WHERE eventTime >= '2024-12-12 05:00:00' AND userIdentity.type = 'AssumedRole' AND eventName in ('ListObjects','PutObject','DeleteObject')
以下のように、結果が整形されて出力されます。これで見やすくなりました!
今回の環境では、Amazon Cognito のログインをメールアドレスにしているため、ユーザー名が UUID になっています。
どの Cognito ユーザーがどのオブジェクトに対してどの操作をしたのか、追跡できるようになりました!
まとめ
いかがでしたでしょうか?
この設定は最低限必須なものだと思いまして、記事にしました。
検索用 SQL づくりが一番苦労しました。ログのデータ型がよくわからず、型変換も入れないと期待する値を取得できませんでした。もっとスマートな方法はあるかもしれません。
本記事が皆様のお役に立てれば幸いです。