In production security, you must maintain an audit log of every API call made in your AWS account. If a resource is deleted, an IAM user is modified, or a security group rule is altered, you must be able to trace who did it, from where, and when. AWS CloudTrail captures these actions, but to act on them in real-time, you must stream those logs to CloudWatch Logs.

In this guide, we'll design a real-time security audit pipeline. We will stream CloudTrail logs to CloudWatch, define metric filters for critical security events, and deploy the entire setup using Terraform.

Incident Prevention: Setting up CloudWatch alarms for unauthorized actions or root logins ensures security teams are notified within seconds of a critical policy breach.

The Architecture

  1. CloudTrail: Captures write/management API actions across all AWS regions.
  2. S3 Bucket: Serves as the immutable, long-term historical audit log repository.
  3. CloudWatch Logs: CloudTrail assumes an IAM role to write logs to a log group, enabling real-time stream analysis and alerting.

Terraform Code: The Audit Pipeline

Let's write the Terraform configuration to provision this secure pipeline. We'll set up the CloudWatch Log Group, S3 storage, IAM log writer roles, and configure the Trail itself.

audit_pipeline.tf
# 1. CloudWatch Log Group for Trails
resource "aws_cloudwatch_log_group" "trail" {
  name              = "/aws/cloudtrail/security-audit"
  retention_in_days = 90
}

# 2. S3 Bucket for Long-term Storage
resource "aws_s3_bucket" "trail" {
  bucket        = "company-audit-logs-222222222222"
  force_destroy = true
}

# 3. IAM Role for CloudTrail logging to CloudWatch
resource "aws_iam_role" "trail" {
  name = "CloudTrailToCloudWatchLoggingRole"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { Service = "cloudtrail.amazonaws.com" }
      Action    = "sts:AssumeRole"
    }]
  })
}

# 4. IAM Policy allowing Log Delivery
resource "aws_iam_role_policy" "trail" {
  name = "CloudTrailToCloudWatchPolicy"
  role = aws_iam_role.trail.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect   = "Allow"
        Action   = ["logs:CreateLogStream", "logs:PutLogEvents"]
        Resource = "${aws_cloudwatch_log_group.trail.arn}:*"
      }
    ]
  })
}

# 5. CloudTrail configuration
resource "aws_cloudtrail" "main" {
  name                          = "organizational-audit-trail"
  s3_bucket_name                = aws_s3_bucket.trail.id
  include_global_service_events = true
  is_multi_region_trail         = true
  enable_log_file_validation    = true

  cloud_watch_logs_group_arn    = "${aws_cloudwatch_log_group.trail.arn}:*"
  cloud_watch_logs_role_arn     = aws_iam_role.trail.arn
}

Step 2: Define Metric Filters for Alerting

With logs streaming to CloudWatch, we configure **Metric Filters** to parse audit records and increment metrics when critical conditions match.

Filter 1: Alert on Unauthorized Operations

Trigger an alert if an API call fails due to permissions (e.g., someone trying to probe resources without access):

  • Filter Pattern: { ($.errorCode = "*UnauthorizedOperation") || ($.errorCode = "AccessDenied") }

Filter 2: Alert on Security Group Modifications

Trigger an alert if security groups are modified (creating public holes in firewalls):

  • Filter Pattern: { ($.eventName = AuthorizeSecurityGroupIngress) || ($.eventName = RevokeSecurityGroupIngress) || ($.eventName = CreateSecurityGroup) }

Here is how we configure the Unauthorized Action filter in Terraform:

metrics.tf
resource "aws_cloudwatch_log_metric_filter" "unauthorized_api" {
  name           = "UnauthorizedAPICallsCount"
  pattern        = "{ ($.errorCode = \"*UnauthorizedOperation\") || ($.errorCode = \"AccessDenied\") }"
  log_group_name = aws_cloudwatch_log_group.trail.name

  metric_transformation {
    name      = "AccessDeniedEvents"
    namespace = "CloudTrailSecurityAudit"
    value     = "1"
  }
}

Step 3: Alertmanager / SNS Notifications

Finally, create a CloudWatch Alarm targeting your metric. If the count of AccessDeniedEvents exceeds 5 within a 5-minute window, the alarm transitions to the ALARM state, routing notifications to an SNS Topic connected to PagerDuty, Slack, or Email channels.

Conclusion

Streaming CloudTrail logs to CloudWatch Logs provides full audit visibility. By codifying this architecture in Terraform and establishing alerting metrics for unauthorized calls, you secure your cloud assets and gain immediate visibility into potential incidents.