【Terraform】Terraformを使用して、S3のCloudTrailデータイベントをオンにする方法を試す

記事タイトルとURLをコピーする

こんにちは。 カスタマーサクセス部の山本です。

CloudTrailを使って、S3バケット内の機密情報が含まれるファイルに対する操作を記録する

S3 バケットに機密情報の入っているファイルを置くとき、そのファイルが誰によって見られたり、改ざんされたりしたかを追跡したいですよね。
CloudTrail はそのような情報を記録するツールです。CloudTrail は、AWSのリソースに対する操作のログを作成します。具体的には、IAMユーザーやIAMロールがAWSマネジメントコンソール、AWS CLI / SDK を使って何を行ったかを記録します。これらの記録は「イベント」と呼ばれます。
イベントには、「管理イベント」と「データイベント」の2つの種類があります。S3 の例を使って説明すると、「管理イベント」は、S3バケットの作成やアクセス権限の変更など、バケット設定の変更を記録します。一方、「データイベント」は、S3バケットにある特定のファイル(オブジェクト)がアップロードやダウンロードされたときのような、オブジェクト操作を記録します。これにより、誰が何を行ったかを追跡することができます。 この記事では、S3における CloudTrail の「データイベント」の設定をTerraformを使用してオンにする方法を紹介します。

「管理イベント」と「データイベント」の詳細については、以下も参照してみてください。

S3 のデータイベントで記録されるもの

S3のデータイベントを取得することで、S3のオブジェクトに対する操作をCloudTrailのログに記録できます。例えば、オブジェクトの取得(GetObject)、削除(DeleteObject)、追加(PutObject)などのAPI操作が該当します。 詳細な情報は、AWSの公式ドキュメントを参照してください。対象となるAPIはすべてオブジェクトに関連しており、現時点(2024年4月2日)で28種類のAPIが対象となっています。 参考:Amazon S3 CloudTrail イベント - Amazon Simple Storage Service

  • AbortMultipartUpload
  • CompleteMultipartUpload
  • CopyObject
  • CreateMultipartUpload
  • DeleteObject
  • DeleteObjectTagging
  • DeleteObjects
  • GetObject
  • GetObjectAcl
  • GetObjectAttributes
  • GetObjectLockLegalHold
  • GetObjectLockRetention
  • GetObjectTagging
  • GetObjectTorrent
  • HeadObject
  • ListMultipartUploads
  • ListObjectVersions
  • ListObjects
  • ListParts
  • PutObject
  • PutObjectAcl
  • PutObjectLockLegalHold
  • PutObjectLockRetention
  • PutObjectTagging
  • RestoreObject
  • SelectObjectContent
  • UploadPart
  • UploadPartCopy

例えば、DeleteObject で削除したオブジェクトの名前も分かるのかな、思っていたものの、Responce には含まないようで、オブジェクト名は分からなそうです。
DeleteObject - Amazon Simple Storage Service

基本的なセレクタ(event_selector)と高度なセレクタ(advanced_event_selector

データイベントを有効にするとき、基本的なセレクタ(event_selector)と高度なセレクタ(advanced_event_selector)の2つから選ぶことができます。しかし、これら2つは同時には使えません。どちらか一方を選びます。 基本的なセレクタ(event_selector)を選ぶと、Lambdaの実行(Invoke)、S3オブジェクトの操作、またはDynamoDBのテーブルの最初の3行に関するログを取得することができます。

For trails, you can use basic or advanced event selectors to log data events for Amazon S3 buckets and bucket objects, Lambda functions, and DynamoDB tables (shown in the first three rows of the table). You can use only advanced event selectors to log the data event types shown in the remaining rows.

参考:Logging data events - AWS CloudTrail

advanced_event_selector (高度なセレクタ) では、AWS Glue、AWS IoT など様々なサービスのログを取得できます。
S3 に関しても GetObject など、特定の API 実行のみを指定してログ取得もできるので、おすすめは高度なセレクタです。
基本的なセレクタ(event_selector)で S3のCloudTrailデータイベントをオンにする方法も参考までに載せます。

Terraformを使用して、S3のCloudTrailデータイベントをオンにする方法を試す

Terraform の公式ドキュメントに載っているサンプルコードを参考に試してみましょう。
Resource: aws_cloudtrail

高度なセレクタ(advanced_event_selector)でデータイベントをオンにする

以下のコードを使ってデータイベントの取得をオンにしました。ただし、このコードでは2箇所でS3バケット名を指定する必要があります。
各バケット名はすべてのリージョン内で一意でなければなりません。

  • 1行目のセクションで指定するのは、機密情報を含むファイルを保存するS3バケット名です。
  • 31行目のセクションでは、CloudTrailのログを保存するためのS3バケット名を指定します。

さらに、13行目から29行目では高度なセレクタ(advanced_event_selector)を設定しました。これにより、機密情報を含むファイルが保存されているS3バケット内の全てのオブジェクトに対するデータイベントを取得するように設定しています。

※ 2024/04/05 追記
21行目の演算子を、equals にしていたものの、starts_with を使用しないと、全てのイベントがログに出ませんでした。そのため、記載を変更しました。
PutObject のイベントだけが出なくて、調べていたら分かりました。以下のドキュメントに記載があります。

AdvancedFieldSelector

To log all data events for all objects in a specific S3 bucket, use the StartsWith operator, and include only the bucket ARN as the matching value.

なお、30行目から36行目では高度なセレクタ(advanced_event_selector)で全ての AWS サービスの管理イベントを有効にしています。不要であればコメントアウトしてください。

# 機密情報が含まれるファイルが保存されるS3バケット
resource "aws_s3_bucket" "important_bucket" {
  bucket = "important-bucket-yamamoto012345678901"
  force_destroy = true
}

# CloudTrail を有効化
resource "aws_cloudtrail" "example" {
  depends_on = [aws_s3_bucket_policy.example,aws_s3_bucket.important_bucket]

  name                          = "example"
  s3_bucket_name                = aws_s3_bucket.example.id
  s3_key_prefix                 = "prefix"
  include_global_service_events = false
  advanced_event_selector {
    name = "Log all S3 objects events except for two S3 buckets"

    field_selector {
      field  = "eventCategory"
      equals = ["Data"]
    }

    field_selector {
      field = "resources.ARN"

      starts_with = [
        "${aws_s3_bucket.important_bucket.arn}/"
      ]
    }

    field_selector {
      field  = "resources.type"
      equals = ["AWS::S3::Object"]
    }
  }
  advanced_event_selector {
    name = "Log readOnly and writeOnly management events"

    field_selector {
      field  = "eventCategory"
      equals = ["Management"]
    }
  }
}

# CloudTrail のログを保存する S3 バケット
resource "aws_s3_bucket" "example" {
  bucket        = "tf-test-trail-yamamoto012345678901"
  force_destroy = true
}

# CloudTrailのログを保存するためのS3バケットに適用するIAMポリシー。このポリシーは、CloudTrailサービスからのデータ追加(PutObject など)操作を許可します。
data "aws_iam_policy_document" "example" {
  statement {
    sid    = "AWSCloudTrailAclCheck"
    effect = "Allow"

    principals {
      type        = "Service"
      identifiers = ["cloudtrail.amazonaws.com"]
    }

    actions   = ["s3:GetBucketAcl"]
    resources = [aws_s3_bucket.example.arn]
    condition {
      test     = "StringEquals"
      variable = "aws:SourceArn"
      values   = ["arn:${data.aws_partition.current.partition}:cloudtrail:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:trail/example"]
    }
  }

  statement {
    sid    = "AWSCloudTrailWrite"
    effect = "Allow"

    principals {
      type        = "Service"
      identifiers = ["cloudtrail.amazonaws.com"]
    }

    actions   = ["s3:PutObject"]
    resources = ["${aws_s3_bucket.example.arn}/prefix/AWSLogs/${data.aws_caller_identity.current.account_id}/*"]

    condition {
      test     = "StringEquals"
      variable = "s3:x-amz-acl"
      values   = ["bucket-owner-full-control"]
    }
    condition {
      test     = "StringEquals"
      variable = "aws:SourceArn"
      values   = ["arn:${data.aws_partition.current.partition}:cloudtrail:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:trail/example"]
    }
  }
}

# バケットポリシー
resource "aws_s3_bucket_policy" "example" {
  bucket = aws_s3_bucket.example.id
  policy = data.aws_iam_policy_document.example.json
}

# AWS アカウント番号を取得
data "aws_caller_identity" "current" {}

# パーティション情報を取得
data "aws_partition" "current" {}

# リージョン情報を取得
data "aws_region" "current" {}

terraform apply 後の AWS マネジメントコンソールです。

編集画面にいくと、高度なセレクタ(advanced_event_selector)だと分かります。

tfstate ファイルを見ても分かります。

(参考) 基本的なセレクタ(event_selector)でデータイベントをオンにする

以下のコードを使ってデータイベントの取得をオンにしました。ただし、このコードでは2箇所でS3バケット名を指定する必要があります。
各バケット名はすべてのリージョン内で一意でなければなりません。

  • 1行目のセクションで指定するのは、機密情報を含むファイルを保存するS3バケット名です。
  • 24行目のセクションでは、CloudTrailのログを保存するためのS3バケット名を指定します。

ここまでは高度なセレクタ(advanced_event_selector)と同じです。
そして、13行目から22行目では基本的なセレクタ(event_selector)を設定しました。これにより、機密情報を含むファイルが保存されているS3バケット内の全てのオブジェクトに対するデータイベントを取得するように設定しています。

なお、12行目では基本的なセレクタ(event_selector)で全ての AWS サービスの管理イベントを有効にしています。不要であればinclude_management_events = false としてください。

# 機密情報が含まれるファイルが保存されるS3バケット
resource "aws_s3_bucket" "important_bucket" {
  bucket = "important-bucket-yamamoto012345678901"
  force_destroy = true
}

# CloudTrail を有効化
resource "aws_cloudtrail" "example" {
  depends_on = [aws_s3_bucket_policy.example,aws_s3_bucket.important_bucket]

  name                          = "example"
  s3_bucket_name                = aws_s3_bucket.example.id
  s3_key_prefix                 = "prefix"
  include_global_service_events = false
  event_selector {
    read_write_type           = "All"
    include_management_events = true

    data_resource {
      type = "AWS::S3::Object"

      # Make sure to append a trailing '/' to your ARN if you want
      # to monitor all objects in a bucket.
      values = ["${aws_s3_bucket.important_bucket.arn}/"]
    }
  }
}

# CloudTrail のログを保存する S3 バケット
resource "aws_s3_bucket" "example" {
  bucket        = "tf-test-trail-yamamoto012345678901"
  force_destroy = true
}

# CloudTrailのログを保存するためのS3バケットに適用するIAMポリシー。このポリシーは、CloudTrailサービスからのデータ追加(PutObject など)操作を許可します。
data "aws_iam_policy_document" "example" {
  statement {
    sid    = "AWSCloudTrailAclCheck"
    effect = "Allow"

    principals {
      type        = "Service"
      identifiers = ["cloudtrail.amazonaws.com"]
    }

    actions   = ["s3:GetBucketAcl"]
    resources = [aws_s3_bucket.example.arn]
    condition {
      test     = "StringEquals"
      variable = "aws:SourceArn"
      values   = ["arn:${data.aws_partition.current.partition}:cloudtrail:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:trail/example"]
    }
  }

  statement {
    sid    = "AWSCloudTrailWrite"
    effect = "Allow"

    principals {
      type        = "Service"
      identifiers = ["cloudtrail.amazonaws.com"]
    }

    actions   = ["s3:PutObject"]
    resources = ["${aws_s3_bucket.example.arn}/prefix/AWSLogs/${data.aws_caller_identity.current.account_id}/*"]

    condition {
      test     = "StringEquals"
      variable = "s3:x-amz-acl"
      values   = ["bucket-owner-full-control"]
    }
    condition {
      test     = "StringEquals"
      variable = "aws:SourceArn"
      values   = ["arn:${data.aws_partition.current.partition}:cloudtrail:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:trail/example"]
    }
  }
}

# バケットポリシー
resource "aws_s3_bucket_policy" "example" {
  bucket = aws_s3_bucket.example.id
  policy = data.aws_iam_policy_document.example.json
}

# AWS アカウント番号を取得
data "aws_caller_identity" "current" {}

# パーティション情報を取得
data "aws_partition" "current" {}

# リージョン情報を取得
data "aws_region" "current" {}

terraform apply 後の AWS マネジメントコンソールです。

編集画面にいくと、基本的なセレクタ(event_selector)だと分かります。

tfstate ファイルを見ても分かります。

S3のCloudTrailデータイベントを確認する。

有効にした S3 のデータイベントを確認するには、CloudWatch Logs に出力するか、Athena のテーブル定義を作成してクエリを実行するかの二通りがあります。
参考:CloudTrail Event 履歴で S3 オブジェクトレベルのイベントを検索する | AWS re:Post

CloudWatch Logs での保管は S3 よりも料金がかかるため、詳細は割愛するものの、紹介します。
CloudWatch Logs に出力すると、CloudWatch の機能を使って、メール/チャット通知ができるので、特定のイベントのみ、CloudWatch Logs に出力するという構成は取り得ます。

(参考)CloudWatch Logs に出力する

CloudWatch Logs に出力するには、Terraform のaws_cloudtrail リソースに定義を追加します。
サンプルコードを抜粋します。

resource "aws_cloudwatch_log_group" "example" {
  name = "Example"
}

resource "aws_cloudtrail" "example" {
  # 他の設定に加えて、以下の行を追加
  cloud_watch_logs_group_arn = "${aws_cloudwatch_log_group.example.arn}:*" # CloudTrail requires the Log Stream wildcard
}

エラーが出たので確認すると、cloud_watch_logs_role_arn も必要でした。

resource "aws_cloudwatch_log_group" "example" {
  name = "Example"
}

resource "aws_cloudtrail" "example" {
  # 他の設定に加えて、以下の行を追加
  cloud_watch_logs_group_arn = "${aws_cloudwatch_log_group.default.arn}:*"
  cloud_watch_logs_role_arn     = aws_iam_role.default.arn
}

CloudWatch Logs での保管は料金もかかるため、IAM ロールに必要な権限については割愛します。
権限については以下のドキュメントを参照ください。
参考:CloudWatch Logs へのイベントの送信 - AWS CloudTrail

CloudWatch Logs のフィルタ構文で、{$.eventName = "GetObject"} のように検索すると、オブジェクトの取得 (GetObject)に関するログを参照できました。

Athena のテーブル定義を作成してクエリを実行する

CloudTrail の「イベント履歴」画面から「Athena テーブルを作成」ボタンを押すと、DDL が出ます。
AWS CloudTrail ログを Amazon Athena で検索するための基本的なテーブル定義を提供しています。

「ストレージの場所」はプルダウンから選択できます。CloudTrail のデータイベント保管先のバケット・プレフィックスを選びます。
「テーブルを作成」ボタンを押します。

CloudTrail ログは非常に複雑な JSON データ構造を持つため、全ての情報を取得するためのテーブル定義が難しいことがあります。
テーブル定義を自動取得するために Glue の クローラー をTerrafrom のコードで作ってみたものの、思うように動作せず、悩みました。(クローラーが失敗しました。)結果、マネジメントコンソールにある「Athena テーブルを作成」ボタンを使用しました。
以下のナレッジベースに共有されている方法です。
Athena テーブルを使用して CloudTrail ログを検索する | AWS re:Post

さて、Athena の画面で、対象のテーブルを選択し、Select 文を実行してみた結果です。

SELECT * FROM <テーブルの名前>;

S3 のデータイベント以外の管理イベントの情報が多いので、S3 の データイベントに絞ってみます。

SELECT * FROM <テーブルの名前> WHERE eventname LIKE '%Object%';

私がファイル削除した DeleteObjects のログが見つかりました。🎉
なお、Delete したオブジェクトの名前は、Responce には含まないようで、わかりませんでした。
DeleteObject - Amazon Simple Storage Service

CloudTrail Lake との比較

この記事では、aws_cloudtrail リソースを利用して、CloudTrailの証跡を作成しました。CloudTrailには、Athenaを使用してテーブルを作成せずに直接CloudTrailをクエリできる新しい機能、 CloudTrail Lake もあります。 これはマネジメントコンソールで利用可能です。

Terraform でもaws_cloudtrail_event_data_store リソースを作成することでCloudTrail Lakeを利用可能です。しかし、今回は以下の理由からaws_cloudtrailを使用しました。

  • SELECT * FROM テーブルした結果が、Athena での実行結果と同じだったため。
    • この記事の目的であるS3のデータイベントを検索するにはAthenaで十分でした
  • 料金体系が少し複雑であり、保管料金が毎月ではなく一括で課金されるため。

最後に、特定の時間にS3オブジェクトを操作したユーザー情報を抽出するクエリの実行結果を示します。このクエリは日時、IPアドレス、IAM認証情報、イベント名、ユーザーエージェントを抽出します。

SELECT
    eventtime, sourceipaddress, userIdentity.arn AS user, eventName, useragent
FROM
    <テーブル名>
WHERE
    userIdentity.arn IS NOT NULL
    AND eventName LIKE '%Object%'
    AND eventTime > '2024-03-27 00:00:00' AND eventTime < '2024-04-05 00:00:00'
  • Athena での実行結果

  • CloudTrail Lake での実行結果

同じですね。
CloudTrail Lake はサンプルクエリが充実していたり、UI も Athena よりちょっとハイライトが効いていたりして、便利でした。

CloudTrail Lake を検証したら、またブログを書こうと思います。

S3 サーバーアクセスログとの比較

参考:サーバーアクセスログによるリクエストのログ記録 - Amazon Simple Storage Service

サーバーアクセスログにも AWS マネジメントコンソール や AWS CLI/SDK からの操作は出るようでした。
ご覧の通り、API の名前 (GetObject) や、sessioncontext の情報がないなど、情報としては少なめでした。
以下に GetObject を AWS マネジメントコンソールで実行したログを抜粋します。

e29b9e948d96bbde869ccdaf25061ba168af826a897bfe808ec9de13cc493d09 important-bucket-yamamoto012345678901 [04/Apr/2024:06:03:44 +0000] 106.xxx.xxx.xxx arn:aws:iam::123456789012:user/administrator E09MQZBHTY9BFFCZ REST.GET.VERSIONING - "GET /important-bucket-yamamoto012345678901?versioning= HTTP/1.1" 200 - 113 - 21 - "-" "S3Console/0.4, aws-internal/3 aws-sdk-java/1.12.488 Linux/5.10.210-178.855.amzn2int.x86_64 OpenJDK_64-Bit_Server_VM/25.372-b08 java/1.8.0_372 vendor/Oracle_Corporation cfg/retry-mode/standard" - nSxsDHqxjjr403fo8yhOCipK09G5XaTHCHFdt0Id3GiwRIdYfxfuvOfU7W5Tx7nL2s86NGvZ1aQ= SigV4 ECDHE-RSA-AES128-GCM-SHA256 AuthHeader s3.ap-northeast-1.amazonaws.com TLSv1.2 - -

まとめ

Terraformを使用して、S3のCloudTrailデータイベントをオンにする方法を試してみました。

山本 哲也 (記事一覧)

カスタマーサクセス部のエンジニア(一応)

好きなサービス:ECS、ALB

趣味:トレラン、登山(たまに)