🐾

Terraform : CloudTrailで特定のAWS APIコールを監視する

2025/02/22に公開

概要

CloudTrail + CloudWatch/EventBridgeでAWS APIコールを監視し、特定の操作がなされた場合にはメール通知する機能を業務で実装したため、備忘録としてTerraformで構築してみました。基本的には、以下に示すAWS BLEA(Baseline Environment on AWS)に基づいて監視対象となる操作を決定しています。

https://github.com/aws-samples/baseline-environment-on-aws/tree/main

構成図

Logs+Metrics Filter と EventBridge Ruleの使い分け

CloudTrailで管理しているAPIコールログを監視する方法としては、主に以下の二つがあります。

  • CloudTrail→CloudWatch Logs→CloudWatch Alarm→SNS
  • CloudTrail→EventBridge→SNS

前者を使用するとCloudWatch Logsを経由する分の余計な費用や遅延が発生してしまうため、基本的には後者を使用するべきです(CloudWatch Logsのデータ取り込み費用は0.76USD/GBと高め)。
しかし、IAMに関するAPIコールイベントはus-east-1で発生し、同リージョンのデフォルトのイベントバスに送信されるため、ap-northeast-1に作成したEventBridge RuleではIAMイベントを拾ってくることができません。そこで、IAMイベントを監視するために、APIコールログをCloudWatch Logsに配信するという前者の設計も同時に採用しています。
また、EventBridge Ruleでは該当のAPIコールが一度でも発生すると、SNS通知がされてしまう一方で、CloudWatch Logs+CloudWatch Alarmの構成では単位時間あたりのAPIコール回数に応じてSNS通知するかを制御することができます。この仕様の違いもどちらを使うかの判断基準になるかと思います。

実装

Terraformコードは下記GitHubに記載しています。本記事では一部を抜粋して紹介させていただきますので、詳細はGitHubをご確認ください。

ディレクトリ構成(抜粋)
├─ environments
│  ├─ prd
│  ├─ stg
│  └─ dev
│     ├─ main.tf
│     ├─ local.tf
│     ├─ variable.tf
│     └─ (terraform.tfvars)
│
└─ modules(各Moduleにmain.tf,variable.tf,output.tfが存在)
      ├─ cloudtrail(Trail証跡と格納先S3バケット/CloudWatch Logs)
      ├─ initializer(tfstate用S3バケット作成)
      └─ monitoring(Metrics FilterやAlarm、EventBridgeなど監視関連のリソース)

https://github.com/kawakami23/AWS_Trail_Monitoring_HCL

IAMポリシー変更検知

インラインポリシの追加・削除、IAMポリシーの作成・削除、IAMポリシーのアタッチ・デタッチ、IAMポリシの新しいバージョンの作成・削除が発生した際に、IAMPolicyEventCountメトリクスに値を発行するメトリクスフィルタを定義しています。基本的にIAMポリシーは初期構築時のみ作成し、運用時に変更されることはないため、一度でも本メトリクスが発行されると、SNS通知するような設計としています。

IAMポリシー変更検知
# IAM Policy Change Detection
resource "aws_cloudwatch_log_metric_filter" "iam_policy_change_filter" {
  name           = "${var.common.env}-IAMPolicyChangeFilter"
  pattern        = <<EOT
  {($.eventName = DeleteGroupPolicy)||
  ($.eventName = DeleteRolePolicy)||
  ($.eventName = DeleteUserPolicy)||
  ($.eventName = PutGroupPolicy)||
  ($.eventName = PutRolePolicy)||
  ($.eventName = PutUserPolicy)||
  ($.eventName = CreatePolicy)||
  ($.eventName = DeletePolicy)||
  ($.eventName = CreatePolicyVersion)||
  ($.eventName = DeletePolicyVersion)||
  ($.eventName = AttachRolePolicy)||
  ($.eventName = DetachRolePolicy)||
  ($.eventName = AttachUserPolicy)||
  ($.eventName = DetachUserPolicy)||
  ($.eventName = AttachGroupPolicy)||
  ($.eventName = DetachGroupPolicy)}
  EOT
  log_group_name = var.cloudtrail.log_group_name
  metric_transformation {
    namespace = "CloudTrailMetrics"
    name      = "IAMPolicyEventCount"
    value     = "1"
  }
}

resource "aws_cloudwatch_metric_alarm" "iam_policy_change_alarm" {
  alarm_name          = "${var.common.env}-IAMPolicyChangeAlarm"
  namespace           = aws_cloudwatch_log_metric_filter.iam_policy_change_filter.metric_transformation[0].namespace
  metric_name         = aws_cloudwatch_log_metric_filter.iam_policy_change_filter.metric_transformation[0].name
  period              = "300"
  statistic           = "Sum"
  evaluation_periods  = "1"
  datapoints_to_alarm = "1"
  threshold           = "1"
  comparison_operator = "GreaterThanOrEqualToThreshold"
  alarm_description   = "IAM Configuration changes detected!"
  alarm_actions       = [aws_sns_topic.main.arn]
}

アクセスキー作成検知

IAMユーザのアクセスキーが作成された際に、NewAccessKeyCreatedEventCountメトリクスに値を発行するメトリクスフィルタです。基本的にアクセスキーは初期構築時のみ作成し、運用時に作成されることはないため、一度でも本メトリクスが発行されると、SNS通知するような設計としています。

アクセスキー作成検知
# New AccessKey Creation Deltection
resource "aws_cloudwatch_log_metric_filter" "new_access_key_created_filter" {
  name           = "${var.common.env}-NewAccessKeyCreatedFilter"
  pattern        = <<EOT
  {($.eventName = CreateAccessKey)}
  EOT
  log_group_name = var.cloudtrail.log_group_name
  metric_transformation {
    namespace = "CloudTrailMetrics"
    name      = "NewAccessKeyCreatedEventCount"
    value     = "1"
  }
}

resource "aws_cloudwatch_metric_alarm" "new_access_key_created_alarm" {
  alarm_name          = "${var.common.env}-NewAccessKeyCreatedAlarm"
  namespace           = aws_cloudwatch_log_metric_filter.new_access_key_created_filter.metric_transformation[0].namespace
  metric_name         = aws_cloudwatch_log_metric_filter.new_access_key_created_filter.metric_transformation[0].name
  period              = "300"
  statistic           = "Sum"
  evaluation_periods  = "1"
  datapoints_to_alarm = "1"
  threshold           = "1"
  comparison_operator = "GreaterThanOrEqualToThreshold"
  alarm_description   = "Warning: New IAM access Key was created. Please be sure this action was neccessary."
  alarm_actions       = [aws_sns_topic.main.arn]
}

ルートユーザによる操作検知

ルートユーザによる任意の操作が行われた際に、RootUserActivityEventCountメトリクスに値を発行するメトリクスフィルタです。AWSではルートユーザによる操作は基本的に行わず、IAMユーザによって操作を行うため、一度でも本メトリクスが発行されると、SNS通知するような設計としています。

ルートユーザによる操作検知
# Root User Activity Detection
resource "aws_cloudwatch_log_metric_filter" "root_user_activity_filter" {
  name           = "${var.common.env}-RootUserActivityFilter"
  pattern        = <<EOT
  {$.userIdentity.type = "Root" && 
  $.userIdentity.invokedBy NOT EXISTS && 
  $.eventType != "AwsServiceEvent"}
  EOT
  log_group_name = var.cloudtrail.log_group_name
  metric_transformation {
    namespace = "CloudTrailMetrics"
    name      = "RootUserActivityEventCount"
    value     = "1"
  }
}

resource "aws_cloudwatch_metric_alarm" "root_user_activity_alarm" {
  alarm_name          = "${var.common.env}-RootUserActivityAlarm"
  namespace           = aws_cloudwatch_log_metric_filter.root_user_activity_filter.metric_transformation[0].namespace
  metric_name         = aws_cloudwatch_log_metric_filter.root_user_activity_filter.metric_transformation[0].name
  period              = "300"
  statistic           = "Sum"
  evaluation_periods  = "1"
  datapoints_to_alarm = "1"
  threshold           = "1"
  comparison_operator = "GreaterThanOrEqualToThreshold"
  alarm_description   = "Root user activity detected!"
  alarm_actions       = [aws_sns_topic.main.arn]
}

SG変更検知

SGに対するインバウンドルールの追加・削除、アウトバウンドルールの追加・削除が発生した際に通知するルールです。基本的にSGは初期構築時のみ作成し、運用時に変更されることはないため、一度でも本イベントが発生すると、SNS通知するような設計となっています。

SG変更検知
# Security Group Change Detection
resource "aws_cloudwatch_event_rule" "sg_change_event_rule" {
  name          = "${var.common.env}-SgChangeEventRule"
  description   = "Notify to create, update or delete a Security Group."
  event_pattern = <<EOT
  {
    "source": ["aws.ec2"],
    "detail-type": ["AWS API Call via CloudTrail"],
    "detail": {
      "eventSource": ["ec2.amazonaws.com"],
      "eventName": [
        "AuthorizeSecurityGroupIngress", 
        "AuthorizeSecurityGroupEgress", 
        "RevokeSecurityGroupIngress", 
        "RevokeSecurityGroupEgress"
      ]
    }
  }
  EOT
}

resource "aws_cloudwatch_event_target" "sg_change_event_rule" {
  rule      = aws_cloudwatch_event_rule.sg_change_event_rule.name
  target_id = "SgChangeTarget"
  arn       = aws_sns_topic.main.arn
}

NACL変更検知

NACLの作成・削除、NACLエントリの追加・削除・置換、NACLを関連付けるサブネットの変更が発生した際に通知するルールです。基本的にNACLは初期構築時のみ作成し、運用時に変更されることはないため、一度でも本イベントが発生すると、SNS通知するような設計となっています。

NACL変更検知
# NACL Change Detection
resource "aws_cloudwatch_event_rule" "nacl_change_event_rule" {
  name          = "${var.common.env}-NACLChangeEventRule"
  description   = "Notify to create, update or delete a Network ACL."
  event_pattern = <<EOT
  {
    "source": ["aws.ec2"],
    "detail-type": ["AWS API Call via CloudTrail"],
    "detail": {
      "eventSource": ["ec2.amazonaws.com"],
      "eventName": [
        "CreateNetworkAcl", 
        "CreateNetworkAclEntry", 
        "DeleteNetworkAcl", 
        "DeleteNetworkAclEntry", 
        "ReplaceNetworkAclAssociation", 
        "ReplaceNetworkAclEntry"
      ]
    }
  }
  EOT
}

resource "aws_cloudwatch_event_target" "nacl_change_event_rule" {
  rule      = aws_cloudwatch_event_rule.nacl_change_event_rule.name
  target_id = "NACLChangeTarget"
  arn       = aws_sns_topic.main.arn
}

CloudTrail設定変更検知

ログ記録の停止、CloudTrailの設定変更、CloudTrail証跡の削除が発生した際に通知するルールです。基本的にCloudTrailは運用時に変更されることはないため、一度でも本イベントが発生すると、SNS通知するような設計となっています。

CloudTrail設定変更検知
# CloudTrail Change Detection
resource "aws_cloudwatch_event_rule" "cloudtrail_change_event_rule" {
  name          = "${var.common.env}-CloudTrailChangeEventRule"
  description   = "Notify to change on CloudTrail log configuration."
  event_pattern = <<EOT
  {
    "detail-type": ["AWS API Call via CloudTrail"],
    "detail": {
      "eventSource": ["cloudtrail.amazonaws.com"],
      "eventName": [
        "StopLogging", 
        "DeleteTrail", 
        "UpdateTrail"
      ]
    }
  }
  EOT
}

resource "aws_cloudwatch_event_target" "cloudtrail_change_event_rule" {
  rule      = aws_cloudwatch_event_rule.cloudtrail_change_event_rule.name
  target_id = "CloudTrailChangeTarget"
  arn       = aws_sns_topic.main.arn
}

動作確認

IAMポリシー変更検知

test_policyという名前のIAMポリシーを作成すると、格納先に指定しているCloudWatch Logsグループに以下のようなAPIコールログが出力されることが確認できます。

そして、CloudWatch Alarmを見てみると、IAMPolicyEventCountのメトリクス値が1を超えたタイミングでアラーム状態に遷移しました!

SG変更検知

test_SGという名前のSGを作成すると、デフォルトのイベントバスにAPIコールイベントが送信され、該当のEventBridge Ruleで検出されることが確認できます。

最後に

費用との相談になりますが、Logs+CloudWatch Metrics Filterの構成の方が柔軟に設計できるので個人的には好みですね。us-east-1に記録されるイベントに関しても、単一リージョンで管理することができますし。

備忘

TerraformでTrail証跡+APIコールログ格納用S3バケットを作成しようとすると、S3バケット→Trail証跡→S3バケットポリシの順に作成されてしまうようです。これでは、S3バケットに適切なポリシが定義されていないためにTrail証跡の作成が失敗してしまいます。これを回避するためにTrail証跡の作成においてdepends_onを定義し、S3バケットポリシを先に作成させてあげる必要があります

aws_cloudtrailでdepends_onを定義する
# Define CloudTrail trail
resource "aws_cloudtrail" "main" {
  name                          = "${var.common.env}-cloudtrail"
  s3_bucket_name                = aws_s3_bucket.cloudtrail.bucket
  include_global_service_events = true
  enable_log_file_validation    = true
  is_multi_region_trail         = true
  cloud_watch_logs_role_arn     = aws_iam_role.cloudtrail.arn
  cloud_watch_logs_group_arn    = "${aws_cloudwatch_log_group.cloudtrail.arn}:*"
  depends_on                    = [aws_s3_bucket_policy.cloudtrail]
}

Discussion