IAM Identity Center ユーザーの S3 オブジェクト操作を Athena で追跡する

## はじめに
IAM Identity Center で SSO ログインしたユーザーの S3 オブジェクト操作をモニタリングしたい場合は、CloudTrail のデータべイベントの `useridentity.principalid` や `useridentity.arn` に着目する必要があります。


```sql:クエリ例
SELECT
    eventtime,
    useridentity.principalid,
    useridentity.arn,
    sourceipaddress,
    useragent,
    requestparameters
FROM cloudtrail_logs_pp
WHERE eventtime >= '2025-04-28T06:00:00Z' 
    AND eventname in ('GetObject','PutObject', 'DeleteObject', 'DeleteObjects')
    AND eventSource = 's3.amazonaws.com'
    AND useridentity.type = 'AssumedRole'
    AND useragent != 'athena.amazonaws.com';
```

## CloudTrail イベントの useridentity エレメントの違い
以下のドキュメントに例が記載されています。

https://docs.aws.amazon.com/ja_jp/awscloudtrail/latest/userguide/cloudtrail-event-reference-user-identity.html

### IAM ユーザーによる操作 ("type": "IAMUser")
`userIdentity.userName` に IAM ユーザー名が記録されます。
```json
"userIdentity": {
    "type": "IAMUser",
    "principalId": "AIDAJ45Q7YFFAREXAMPLE",
    "arn": "arn:aws:iam::123456789012:user/Alice",
    "accountId": "123456789012",
    "accessKeyId": "",
    "userName": "Alice"
}
```

### SSO ユーザーによる操作 ("type": "AssumedRole")
SSO ログインの場合には、内部的には STS による Assume Role でのアクセスになるため、username には値が記録されません。そのため、ロールセッション名を確認できる `useridentity.principalid` や `useridentity.arn` を参照します。

 `userIdentity.sessionContext.serssionIssuer` で参照できる arn や userName はロール名のみが記録されるため、ユーザーまでは把握できません。

```json
"userIdentity": {
    "type": "AssumedRole",
    "principalId": "AROAIDPPEZS35WEXAMPLE:AssumedRoleSessionName",
    "arn": "arn:aws:sts::123456789012:assumed-role/RoleToBeAssumed/MySessionName",
    "accountId": "123456789012",
    "accessKeyId": "",
    "sessionContext": {
        "sessionIssuer": {
            "type": "Role",
            "principalId": "AROAIDPPEZS35WEXAMPLE",
            "arn": "arn:aws:iam::123456789012:role/RoleToBeAssumed",
            "accountId": "123456789012",
            "userName": "RoleToBeAssumed"
        },
        "attributes": {
            "mfaAuthenticated": "false",
            "creationDate": "20131102T010628Z"
        }    
    }
}
```

### 参考: IAM Identity Center ユーザーに代わって行われたリクエスト ("type": "IdentityCenterUser")

```json
"userIdentity": {
    "type": "IdentityCenterUser",
    "accountId": "123456789012",
    "onBehalfOf": {
        "userId": "544894e8-80c1-707f-60e3-3ba6510dfac1",
        "identityStoreArn": "arn:aws:identitystore::123456789012:identitystore/d-9067642ac7" 
    },
    "credentialId": "EXAMPLEVHULjJdTUdPJfofVa1sufHDoj7aYcOYcxFVllWR_Whr1fEXAMPLE"
}
```
このタイプのリクエストは IAM Identity Center を使用してアプリケーションアクセスを行った場合などに記録されます。例えば Amazon Q Developer (CodeWhisperer) の IDE によるアクティビティをデータイベントとして取得できますが、"type": "IdentityCenterUser" で記録されます。

AWS アカウントにアクセスしたユーザーの操作記録は前述のとおり `"type": "AssumedRole"` として記録されることにご注意ください。

また 2025/7/14 以降、アクセスポータルにおける操作など、これまで userIdentity が Unknown で記録されていたものも `"type": "IdentityCenterUser"` として記録されるように変更される予定です。

変更の対象:
* UserAuthentication などの [IAM Identity Center サインインイベント](https://docs.aws.amazon.com/ja_jp/singlesignon/latest/userguide/understanding-sign-in-events.html)
* Authenticate, GetRoleCredentials などの [AWS アクセスポータル API オペレーション](https://docs.aws.amazon.com/ja_jp/singlesignon/latest/userguide/sso-info-in-cloudtrail.html#cloudtrail-events-access-portal-operations) 
* CreateToken などの [OpenID Connect (OIDC) イベント](https://docs.aws.amazon.com/singlesignon/latest/userguide/sso-info-in-cloudtrail.html#cloudtrail-events-oidc-operations)

https://aws.amazon.com/jp/blogs/security/modifications-to-aws-cloudtrail-event-data-of-iam-identity-center/

このタイプのリクエストは `userIdentity.onBehalfOf` で IAM Identity Center ユーザーの userId を確認できるほか、additionalEventData 要素で userName も確認できるようです。

https://docs.aws.amazon.com/ja_jp/singlesignon/latest/userguide/sso-cloudtrail-use-cases.html#user-session-iam-identity-center

https://docs.aws.amazon.com/ja_jp/singlesignon/latest/userguide/sign-in-events-examples.html#sign-in-events-examples-2

## 事前に必要な設定
### S3 データイベントの取得設定
CloudTrail 証跡で S3 のオブジェクトレベルの操作を記録するにはデータイベントの記録を有効化する必要があります。

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/cloudtrail-logging-s3-info.html#cloudtrail-data-events

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/01deb036-12e1-4f71-80f1-a58205dc092a.png)

### CloudTrail ログ用テーブルの作成
パーティション射影を使用して Athena で CloudTrail ログ用テーブルを作成します。必要に応じて専用のデータベースを作成し、ドキュメントに記載の CREATE TABLE ステートメントを実行するだけで OK です。

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

パーティション射影を使用することで、必要なパーティションのみを効率的にスキャンできるため、クエリの実行時間が短縮され、スキャンするデータ量が減ってコストも削減されます。


### 参考: S3 サーバーアクセスログ用テーブルの作成
CloudTrail のデータイベントでは DeleteObjects API などバッチオペレーション操作はオブジェクトキーやアクセス拒否 (Access Denied) などの情報が記録されません。必要に応じて S3 サーバーアクセスログを組み合わせて分析することも検討できます。S3 のサーバーアクセスログの出力はベストエフォートである点にはご注意ください。

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/logging-with-S3.html

パーティション射影を使用して S3 のサーバーアクセスログを分析する方法は以下のナレッジに記載があります。

https://repost.aws/knowledge-center/analyze-logs-athena

サーバーアクセスログでパーティション射影を使用する場合はログオブジェクトキーの形式を「分析およびクエリのアプリケーションを高速化するには、この形式を使用します。」の方で選択する必要があります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/e1a38b8e-24d8-4b78-8110-b4b180851050.png)


## 実行例
これまでの情報を踏まえて以下のようなクエリを実行してみます。このクエリでは S3 バケットへのアクセスパターンを分析したり、不正アクセスの調査をしたりすることを前提に、SSO ユーザーによる特定の S3 オブジェクト操作（取得、追加、削除）を抽出します。またアクセス拒否が発生したかどうかを示す access_denied フィールドを追加しています。

```sql
SELECT
    eventtime AS event_time,
    eventname AS event_name,
    awsRegion AS aws_region,
    json_extract_scalar(requestparameters, '$.bucketName') AS bucket_name,
    json_extract_scalar(requestparameters, '$.key') AS object_key,
    useridentity.principalId AS user_id,
    useridentity.arn AS user_arn,
    sourceipaddress AS source_ip,
    useragent AS user_agent,
    CASE 
        WHEN errorcode = 'AccessDenied' THEN 'Yes'
        ELSE 'No'
    END AS access_denied,
    errorcode AS error_code,
    errormessage AS error_message
FROM cloudtrail_logs_pp
WHERE eventtime >= '2025-04-30T06:00:00Z' 
    AND eventname in ('GetObject','PutObject', 'DeleteObject', 'DeleteObjects')
    AND eventSource = 's3.amazonaws.com'
    AND useridentity.type = 'AssumedRole'
    AND useragent NOT LIKE '%.amazonaws.com'  -- AWS サービスのユーザーエージェントを除外
ORDER BY eventtime ASC
```

以下のような実行結果を得ることができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/df8e0230-7e2e-4827-a0c8-f63b708c0c5d.png)

肝心な情報が黒塗りばかりで恐縮ですが、テキストでマスキングすると以下のような結果です。

```
"event_time","event_name","aws_region","bucket_name","object_key","user_id","user_arn","source_ip","user_agent","access_denied","error_code","error_message"
"2025-04-30T16:28:12Z","GetObject","ap-northeast-1","<bucket_name>","test.json","PrincipalID:example@example.com","arn:aws:sts::123456789012:assumed-role/AWSReservedSSO_ViewOnlyAccess_abcd1234efgh5678/example@example.com","xxx.xxx.xxx.xxx","[Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36]","Yes","AccessDenied","User: arn:aws:sts::123456789012:assumed-role/AWSReservedSSO_ViewOnlyAccess_abcd1234efgh5678/example@example.com is not authorized to perform: s3:GetObject on resource: ""arn:aws:s3:::<bucket_name>/test.json"" because no identity-based policy allows the s3:GetObject action"
"2025-04-30T16:28:32Z","DeleteObjects","ap-northeast-1","<bucket_name>",,"PrincipalID:example@example.com","arn:aws:sts::123456789012:assumed-role/AWSReservedSSO_ViewOnlyAccess_abcd1234efgh5678/example@example.com","xxx.xxx.xxx.xxx","[Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36]","No",,
"2025-04-30T17:31:33Z","DeleteObject","ap-northeast-1","<bucket_name>","test.json","PrincipalID:example@example.com","arn:aws:sts::123456789012:assumed-role/AWSReservedSSO_ViewOnlyAccess_abcd1234efgh5678/example@example.com","xxx.xxx.xxx.xxx","[aws-cli/2.25.8 md/awscrt#0.23.8 ua/2.1 os/windows#10 md/arch#amd64 lang/python#3.12.9 md/pyimpl#CPython m/G cfg/retry-mode#standard md/installer#exe md/prompt#off md/command#s3.rm]","Yes","AccessDenied","User: arn:aws:sts::123456789012:assumed-role/AWSReservedSSO_ViewOnlyAccess_abcd1234efgh5678/example@example.com is not authorized to perform: s3:DeleteObject on resource: ""arn:aws:s3:::<bucket_name>/test.json"" because no identity-based policy allows the s3:DeleteObject action"
"2025-05-01T01:26:03Z","PutObject","ap-northeast-1","<bucket_name>","test2.json","PrincipalID:example@example.com","arn:aws:sts::123456789012:assumed-role/AWSReservedSSO_AdministratorAccess_wxyz9876ijkl4321/example@example.com","xxx.xxx.xxx.xxx","[aws-cli/2.26.6 md/awscrt#0.25.4 ua/2.1 os/linux#6.1.132-147.221.amzn2023.x86_64 md/arch#x86_64 lang/python#3.13.2 md/pyimpl#CPython exec-env/CloudShell m/G cfg/retry-mode#standard md/installer#exe md/distrib#amzn.2023 md/prompt#off md/command#s3.cp]","No",,
```

前述の S3 サーバーアクセスログを併用することで、DeleteObjects API によって削除が試行されたオブジェクトキーを特定することもできます。

```sql
WITH cloudtrail_events AS (
    SELECT
        eventtime AS event_time,
        eventname AS event_name,
        awsRegion AS aws_region,
        json_extract_scalar(requestparameters, '$.bucketName') AS bucket_name,
        json_extract_scalar(requestparameters, '$.key') AS object_key,
        useridentity.principalId AS user_id,
        useridentity.arn AS user_arn,
        sourceipaddress AS source_ip,
        useragent AS user_agent,
        CASE 
            WHEN errorcode = 'AccessDenied' THEN 'Yes'
            ELSE 'No'
        END AS access_denied,
        errorcode AS error_code,
        errormessage AS error_message,
        requestid AS request_id
    FROM cloudtrail_logs_pp
    WHERE eventtime >= '2025-05-01T00:00:00Z' 
        AND eventname = 'DeleteObjects'
        AND eventSource = 's3.amazonaws.com'
        AND useridentity.type = 'AssumedRole'
        AND useragent NOT LIKE '%.amazonaws.com'
),

s3_logs AS (
    SELECT
        parse_datetime(requestdatetime, 'dd/MMM/yyyy:HH:mm:ss Z') AS log_time,
        bucket_name,
        remoteip AS remote_ip,
        requester,
        operation,
        key AS object_key,
        httpstatus,
        errorcode AS s3_error_code,
        useragent AS s3_user_agent,
        requestid AS request_id
    FROM server_access_logs_pp
    WHERE timestamp >= '2025/05/01'
        AND operation = 'BATCH.DELETE.OBJECT'
)

-- CloudTrail データイベントと S3 サーバーアクセスログを結合
SELECT
    CASE 
        WHEN c.event_time IS NOT NULL THEN CAST(c.event_time AS varchar)
        ELSE CAST(s.log_time AS varchar)
    END AS operation_time,
    COALESCE(c.bucket_name, s.bucket_name) AS bucket_name,
    s.operation,
    COALESCE(c.object_key, s.object_key) AS object_key,
    c.user_arn,
    COALESCE(c.source_ip, s.remote_ip) AS source_ip,
    COALESCE(c.user_agent, s.s3_user_agent) AS user_agent,
    s.httpstatus,
    CASE 
        WHEN s.httpstatus = '403' OR c.access_denied = 'Yes' THEN 'Yes'
        ELSE 'No'
    END AS access_denied,
    COALESCE(c.error_code, s.s3_error_code) AS error_code,
    c.error_message,
    c.request_id
FROM cloudtrail_events c
FULL OUTER JOIN s3_logs s ON c.request_id = s.request_id
ORDER BY operation_time ASC
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/e90e5cf7-01ff-402d-a963-e629d7f4e2f7.png)

以上です。
参考になれば幸いです。