Athena で CloudTrail ログ(S3 データイベント)を直接クエリして、IAM ユーザーやロールが操作したオブジェクトを特定する

Athena で CloudTrail ログ(S3 データイベント)を直接クエリして、IAM ユーザーやロールが操作したオブジェクトを特定する

CloudTrail の S3 データイベントを Amazon Athena で直接クエリし、特定の IAM エンティティがどのオブジェクトに対してどの操作を行ったかを特定する方法を解説します。IAM Access Analyzer のポリシー生成ではデータイベントを扱えないため、データイベントまで踏み込んだ最小権限化にはこの方法が有効です。
2026.09.04

はじめに

猫とアポロチョコが好きな m.hayakawa です。

今回は意外と準備が大変な Athena での CloudTrail 証跡ログのクエリについてやってみます。

管理イベントについてのクエリについてはいくつか記事があると思いますが、データイベントをどうクエリするかという内容の記事がなかったため、やってみようの精神で、本記事にてご紹介します。

前提知識: データイベントをクエリするケースについて

まず、どういったシーンでデータイベントをクエリしたくなるでしょうか。

アプリケーションから S3 へのアクセスに失敗した際の原因調査をするということがすぐに浮かびますが、今回はアプリケーションが使用している IAM エンティティの IAM ポリシーを最小化したい場合を想定とします。

IAM ユーザーの権限を最小化する際、IAM Access Analyzer のポリシー生成が便利ですが、この機能は管理イベントのみが対象となり、S3 の GetObjectPutObject などのデータイベントを分析対象に含みません。

https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/access-analyzer-policy-generation.html

データイベントを使用できません — IAM Access Analyzer は、生成されたポリシーで、Amazon S3 データイベントなどのデータイベントのアクションレベルのアクティビティを識別しません。

そのため、データイベントを主に使う IAM ユーザーの権限を、実際に使ったアクションやリソース単位まで詰めようとすると、ポリシー生成だけでは不十分です。

本記事で紹介すること

本記事では、CloudTrail の S3 データイベントを Athena で直接クエリし、次の 2 点を確認する方法を解説します。

  • 対象の IAM ユーザーが、どの S3 データイベント(GetObjectPutObject など)を実行したか
  • どのバケット・オブジェクト(リソース ARN)に対して実行したか

データイベントの対象サービスは代表的なものとして下記がありますが、本記事では対象外とします(いずれ記事化するかもしれません)。

  • Amazon SNS
  • Publish など
  • Amazon CloudWatch
    • メトリクスに対する API
  • DynamoDB
    • PutItem, DeleteItem, UpdateItem などのアイテムレベルの API

やってみた

本記事では、検証も兼ねて次の流れで進めます。

  1. CloudFormation で、データイベントの対象バケット・ログ保存先バケット・CloudTrail 証跡を作成する
  2. CloudTrail のマネジメントコンソールから Athena テーブルを作成する
  3. CloudFormation の Outputs(出力) で確認した対象バケットへ、CloudShell からオブジェクトをアップロードする
  4. Athena でクエリし、データイベントを確認する

ステップ1. CloudFormation で対象バケットと証跡を作成する

CloudTrail のデータイベントは、記録対象が多いほどコストがかかります。そこで本記事では、検証用の S3 バケットを 1 つ作成し、そのバケット配下のオブジェクトに対する操作のみをデータイベントとして記録します。

CloudFormation で、次のリソースを作成します。

  • データイベントの記録対象となる検証用バケット
    • このバケット内のオブジェクトのみをデータイベントとして記録する CloudTrail 証跡(管理イベントは無効)
  • CloudTrail のログ保存先バケット
CloudFormationテンプレート
AWSTemplateFormatVersion: '2010-09-09'
Description: CloudTrail trail logging S3 data events for a single target bucket only

Resources:
  # データイベントの記録対象となる検証用バケット
  TargetBucket:
    Type: AWS::S3::Bucket
    Properties:
      # スタック ID に含まれる UUID の先頭セグメント(8桁)をバケット名に付与
      BucketName: !Sub
        - 'dataevent-target-${AWS::AccountId}-${Suffix}'
        - Suffix: !Select [0, !Split ['-', !Select [2, !Split ['/', !Ref 'AWS::StackId']]]]
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true

  # CloudTrail のログ保存先バケット
  TrailBucket:
    Type: AWS::S3::Bucket
    Properties:
      # スタック ID に含まれる UUID の先頭セグメント(8桁)をバケット名に付与
      BucketName: !Sub
        - 'cloudtrail-dataevent-${AWS::AccountId}-${Suffix}'
        - Suffix: !Select [0, !Split ['-', !Select [2, !Split ['/', !Ref 'AWS::StackId']]]]
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true

  # CloudTrail がログを書き込むためのバケットポリシー
  TrailBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref TrailBucket
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: AWSCloudTrailAclCheck
            Effect: Allow
            Principal:
              Service: cloudtrail.amazonaws.com
            Action: s3:GetBucketAcl
            Resource: !GetAtt TrailBucket.Arn
            Condition:
              StringEquals:
                aws:SourceArn: !Sub 'arn:aws:cloudtrail:${AWS::Region}:${AWS::AccountId}:trail/dataevent-trail'
          - Sid: AWSCloudTrailWrite
            Effect: Allow
            Principal:
              Service: cloudtrail.amazonaws.com
            Action: s3:PutObject
            Resource: !Sub '${TrailBucket.Arn}/AWSLogs/${AWS::AccountId}/*'
            Condition:
              StringEquals:
                s3:x-amz-acl: bucket-owner-full-control
                aws:SourceArn: !Sub 'arn:aws:cloudtrail:${AWS::Region}:${AWS::AccountId}:trail/dataevent-trail'

  # 検証用バケットのオブジェクトのみをデータイベントとして記録する証跡
  DataEventTrail:
    Type: AWS::CloudTrail::Trail
    DependsOn: TrailBucketPolicy
    Properties:
      TrailName: dataevent-trail
      S3BucketName: !Ref TrailBucket
      IsLogging: true
      IsMultiRegionTrail: false
      IncludeGlobalServiceEvents: false
      AdvancedEventSelectors:
        - Name: S3 data events for target bucket only
          FieldSelectors:
            - Field: eventCategory
              Equals:
                - Data
            - Field: resources.type
              Equals:
                - AWS::S3::Object
            # 検証用バケット配下のオブジェクトに限定してコストを抑える
            - Field: resources.ARN
              StartsWith:
                - !Sub '${TargetBucket.Arn}/'

Outputs:
  TargetBucketName:
    Description: S3 bucket for data event logging
    Value: !Ref TargetBucket
  TrailBucketName:
    Description: S3 bucket for CloudTrail logs
    Value: !Ref TrailBucket

※バケット名の重複を防ぐため、CloudFormation のスタック ID に含まれる UUID の先頭セグメントを付与しています。

上記のテンプレートを、CloudFormation のマネジメントコンソールからそのままデプロイします。今回の検証リージョンは ap-northeast-1 としました。

  1. CloudFormation コンソールを開く
  2. 「スタックの作成」→「新しいリソースを使用(標準)」を選択する
  3. 下記のテンプレートをファイルとして用意し、「テンプレートファイルのアップロード」で指定する
  4. スタック名を cloudtrail-dataevent とする(この後の AWS CLI コマンドで、CloudFormation スタックから各リソース名を取得するため、スタック名は固定としてください。)
  5. 内容を確認してスタックを作成する

作成後、検証用バケット名とログ保存先バケット名は、スタックの Output(出力)タブにある TargetBucketNameTrailBucketName で確認できます。以降の手順では、これらの値を使用します。

ステップ2. Athena テーブルを作成する

CloudTrail のマネジメントコンソールから Athena テーブルを作成します。

  1. CloudTrail コンソールの「イベント履歴」を開く
  2. Athena テーブルの作成画面で、CloudFormation の Outputs(出力) に表示された TrailBucketName の値をログ保存先として指定する
  3. 「テーブルを作成」を押下する

Screenshot-2026-09-02 13_52_51

https://docs.aws.amazon.com/ja_jp/athena/latest/ug/create-cloudtrail-table-ct.html

上記手順で作成されるテーブルは非パーティションです。パーティションを使いたい場合は、Athena で手動作成してください。

https://docs.aws.amazon.com/ja_jp/athena/latest/ug/create-cloudtrail-table.html

ステップ3. AWS CLI で S3 を操作してデータイベントを発生させる

下記の手順を実施します。

  • S3 へのアクセス権限がある IAM ユーザー/ロールでマネジメントコンソールへログイン
  • CloudShell から、データイベント取得対象バケットへオブジェクトをアップロード
# CloudFormation スタックのOutputsからTargetBucketNameを取得
TARGET_BUCKET_NAME=$(aws cloudformation describe-stacks \
  --stack-name cloudtrail-dataevent \
  --region ap-northeast-1 \
  --query "Stacks[0].Outputs[?OutputKey=='TargetBucketName'].OutputValue" \
  --output text)

echo "データイベント対象バケット: ${TARGET_BUCKET_NAME}"

# テスト用ファイルを作成
echo "hello" > testobject.txt

# オブジェクトをアップロード(PutObject: データイベント)
aws s3api put-object \
  --region ap-northeast-1 \
  --bucket "${TARGET_BUCKET_NAME}" \
  --key testobject.txt \
  --body testobject.txt

Screenshot-2026-09-04 11_11_56

ステップ4. Athena でクエリする

データイベントは反映まで数分かかることがあります。
データイベント保存先バケットにオブジェクトが生成されたら、以下のクエリで確認します。

対象 IAM ロールが実行した S3 イベントを抽出する

IAM ロールで操作した場合、useridentity.typeAssumedRole になります。useridentity.arn はロール本体の ARN ではなく、セッション名を含む STS の ARN です。そのため、ロール本体の ARN を保持する useridentity.sessioncontext.sessionissuer.arn で絞り込みます。

SELECT
    eventtime,
    eventsource,
    eventname,
    errorcode,
    resources
FROM <ここはAthenaのテーブル名に置き換え>
WHERE useridentity.type = 'AssumedRole'
    AND useridentity.sessioncontext.sessionissuer.arn = 'arn:aws:iam::123456789012:role/example-role'
    AND eventsource = 's3.amazonaws.com'
ORDER BY eventtime DESC;

なお、クエリ結果は下記のようになりました。

#	eventtime	eventsource	eventname	errorcode	resources
1	2026-09-04T02:11:32Z	s3.amazonaws.com	PutObject		[{arn=arn:aws:s3:::dataevent-target-123456789012-385d36f0, accountid=123456789012, type=AWS::S3::Bucket}, {arn=arn:aws:s3:::dataevent-target-123456789012-385d36f0/testobject.txt, accountid=null, type=AWS::S3::Object}]
2	2026-09-04T02:10:10Z	s3.amazonaws.com	HeadBucket		[{arn=arn:aws:s3:::dataevent-target-123456789012-385d36f0, accountid=123456789012, type=AWS::S3::Bucket}, {arn=null, accountid=null, type=AWS::S3::Object}]
3	2026-09-04T01:56:25Z	s3.amazonaws.com	HeadBucket		[{arn=arn:aws:s3:::dataevent-target-123456789012-385d36f0, accountid=123456789012, type=AWS::S3::Bucket}, {arn=null, accountid=null, type=AWS::S3::Object}]

対象 IAM ユーザーが実行した S3 イベントを抽出する

IAM ユーザーを対象にする場合は、useridentity.type = 'IAMUser'useridentity.arn = 'arn:aws:iam::<アカウントID>:user/<ユーザー名>' の組み合わせに変更します。

SELECT
    eventtime,
    eventsource,
    eventname,
    errorcode,
    resources
FROM <ここはAthenaのテーブル名に置き換え>
WHERE useridentity.type = 'IAMUser'
    AND useridentity.arn = 'arn:aws:iam::123456789012:user/example-user'
    AND eventsource = 's3.amazonaws.com'
ORDER BY eventtime DESC;

123456789012 は対象 AWS アカウントの ID、example-user は対象 IAM ユーザー名に置き換えてください。

なお、クエリ結果は下記のようになりました。

#	eventtime	eventsource	eventname	errorcode	resources
1	2026-09-04T03:08:41Z	s3.amazonaws.com	PutObject		[{arn=arn:aws:s3:::dataevent-target-123456789012-385d36f0, accountid=123456789012, type=AWS::S3::Bucket}, {arn=arn:aws:s3:::dataevent-target-123456789012-385d36f0/testobject2.txt, accountid=null, type=AWS::S3::Object}]

リソース(ARN)単位で確認する

どのリソースに対して実行したかは、resources フィールドから確認します。resourcesARRAY<STRUCT> 型のため、CROSS JOIN UNNEST で展開します。

SELECT
    ct.eventname,
    resource.arn AS resource_arn,
    resource.type AS resource_type,
    count(*) AS event_count
FROM <ここはAthenaのテーブル名に置き換え> AS ct
CROSS JOIN UNNEST(ct.resources) AS t(resource)
WHERE ct.useridentity.type = 'AssumedRole'
    AND ct.useridentity.sessioncontext.sessionissuer.arn = 'arn:aws:iam::123456789012:role/example-role'
    AND ct.eventsource = 's3.amazonaws.com'
    AND ct.errorcode IS NULL
GROUP BY ct.eventname, resource.arn, resource.type
ORDER BY event_count DESC;

今回の場合は、resource.arn を元にアクセス先を絞り込めます。なお、resources は任意フィールドであり、記録される内容はサービスや API によって異なります。すべてのイベントでリソースの ARN が得られるとは限らない点には注意してください。

なお、クエリ結果は下記のようになりました。

#	eventname	resource_arn	resource_type	event_count
1	HeadBucket	arn:aws:s3:::dataevent-target-123456789012-385d36f0	AWS::S3::Bucket	2
2	HeadBucket		AWS::S3::Object	2
3	ListObjects		AWS::S3::Object	1
4	ListObjects	arn:aws:s3:::dataevent-target-123456789012-385d36f0	AWS::S3::Bucket	1
5	PutObject	arn:aws:s3:::dataevent-target-123456789012-385d36f0	AWS::S3::Bucket	1
6	PutObject	arn:aws:s3:::dataevent-target-123456789012-385d36f0/testobject.txt	AWS::S3::Object	1

上記のように、データイベントを元にクエリを行うことができます。最小権限を絞るためのクエリとしては、まず上記を抑えておいて、環境に合わせて応用してくことが考えられます。

後片付け

検証後は、課金とデータイベント記録の停止のためにリソースを削除します。CloudFormation スタック cloudtrail-dataevent の Outputs から、対象バケット名とログ保存先バケット名を取得します。

# CloudFormation スタックのOutputsからバケット名を取得
TARGET_BUCKET_NAME=$(aws cloudformation describe-stacks \
  --stack-name cloudtrail-dataevent \
  --region ap-northeast-1 \
  --query "Stacks[0].Outputs[?OutputKey=='TargetBucketName'].OutputValue" \
  --output text)

TRAIL_BUCKET_NAME=$(aws cloudformation describe-stacks \
  --stack-name cloudtrail-dataevent \
  --region ap-northeast-1 \
  --query "Stacks[0].Outputs[?OutputKey=='TrailBucketName'].OutputValue" \
  --output text)

# 検証用バケットを空にする
aws s3 rm "s3://${TARGET_BUCKET_NAME}" --recursive

# CloudTrail の保存先バケットを空にする
aws s3 rm "s3://${TRAIL_BUCKET_NAME}" --recursive

その後、CloudFormation コンソールで cloudtrail-dataevent スタックを削除します。スタック削除時にバケットが空でないというエラーになった場合は、ログが追加されていないか確認して、両方のバケットを再度空にしてからスタックを削除します。

まとめ

CloudTrail の S3 データイベントを対象バケットに限定して記録し、Athena で直接クエリすることで、IAM Access Analyzer のポリシー生成では拾えないデータイベントと、アクセス対象のリソース ARN を確認できました。

データイベントまで踏み込んで最小権限を検討する場合は、成功した操作だけでなく、拒否された操作や実際のリソース ARN も確認したうえで IAM ポリシーを判断しましょう。

本記事がどこかの誰かに役立ちますように。

参考資料

クラスメソッドオペレーションズ株式会社について

クラスメソッドグループのオペレーション企業です。

運用・保守開発・サポート・情シス・バックオフィスの専門チームが、IT・AI をフル活用した「しくみ」を通じて、お客様の業務代行から課題解決や高付加価値サービスまでを提供するエキスパート集団です。

当社は様々な職種でメンバーを募集しています。

「オペレーション・エクセレンス」と「らしく働く、らしく生きる」を共に実現するカルチャー・しくみ・働き方にご興味がある方は、クラスメソッドオペレーションズ株式会社 コーポレートサイト をぜひご覧ください。※2026 年 1 月 アノテーション㈱から社名変更しました

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事