Terraform : CloudTrailで特定のAWS APIコールを監視する
概要
CloudTrail + CloudWatch/EventBridgeでAWS APIコールを監視し、特定の操作がなされた場合にはメール通知する機能を業務で実装したため、備忘録としてTerraformで構築してみました。基本的には、以下に示すAWS BLEA(Baseline Environment on AWS)に基づいて監視対象となる操作を決定しています。
構成図
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など監視関連のリソース)
IAMポリシー変更検知
インラインポリシの追加・削除、IAMポリシーの作成・削除、IAMポリシーのアタッチ・デタッチ、IAMポリシの新しいバージョンの作成・削除が発生した際に、IAMPolicyEventCount
メトリクスに値を発行するメトリクスフィルタを定義しています。基本的にIAMポリシーは初期構築時のみ作成し、運用時に変更されることはないため、一度でも本メトリクスが発行されると、SNS通知するような設計としています。
# 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通知するような設計となっています。
# 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 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 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バケットポリシを先に作成させてあげる必要があります。
# 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