Storage Browser for Amazon S3 のアクセスログを取得・検索する [AWS CloudTrail 利用]

こんにちは、広野です。

以下の記事で、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 づくりが一番苦労しました。ログのデータ型がよくわからず、型変換も入れないと期待する値を取得できませんでした。もっとスマートな方法はあるかもしれません。

本記事が皆様のお役に立てれば幸いです。

著者について
広野 祐司

AWS サーバーレスアーキテクチャを駆使して社内クラウド人材育成アプリとコンテンツづくりに勤しんでいます。React で SPA を書き始めたら快適すぎて、他の言語には戻れなくなりました。サーバーレス & React 仲間を増やしたいです。AWSは好きですが、それよりもフロントエンド開発の方が好きでして、バックエンド構築を簡単にしてくれたAWSには感謝の気持ちの方が強いです。
取得資格:AWS 認定は13資格、ITサービスマネージャ、ITIL v3 Expert 等
2020 - 2024 Japan AWS Top Engineer 受賞
2022 - 2024 AWS Ambassador 受賞
2023 当社初代フルスタックエンジニア認定
好きなAWSサービス:AWS Amplify / AWS AppSync / Amazon Cognito / AWS Step Functions / AWS CloudFormation

広野 祐司をフォローする

クラウドに強いによるエンジニアブログです。

SCSKクラウドサービス(AWS)は、企業価値の向上につながるAWS 導入を全面支援するオールインワンサービスです。AWS最上位パートナーとして、多種多様な業界のシステム構築実績を持つSCSKが、お客様のDX推進を強力にサポートします。

AWSアプリケーション開発クラウドソリューション運用・監視
シェアする
タイトルとURLをコピーしました