Skip to main content

CloudTrail CloudWatch Logging Enabled

Overview

This check verifies that your AWS CloudTrail trails are sending logs to CloudWatch Logs and that delivery has occurred within the last 24 hours. CloudTrail records API activity in your AWS account, and integrating it with CloudWatch Logs lets you monitor, search, and alert on that activity in near real-time.

Risk

Without CloudWatch Logs integration, your CloudTrail data sits only in S3 buckets, which are harder to search and monitor in real-time. This creates blind spots where:

  • Unauthorized API calls can go unnoticed for hours or days
  • Privilege escalation attempts may not trigger alerts
  • Data exfiltration through API misuse can occur without detection
  • Incident response is delayed because logs are not readily searchable

Remediation Steps

Prerequisites

You need:

  • AWS Console access with permissions to modify CloudTrail and create IAM roles
  • An existing CloudTrail trail (or permission to create one)
Required IAM permissions (for administrators)

Your IAM user or role needs these permissions:

  • cloudtrail:UpdateTrail
  • cloudtrail:DescribeTrails
  • logs:CreateLogGroup
  • logs:DescribeLogGroups
  • iam:CreateRole
  • iam:PutRolePolicy
  • iam:PassRole

AWS Console Method

  1. Open CloudTrail in the AWS Console

  2. Select your trail

    • Click Trails in the left sidebar
    • Click on the trail name you want to configure
  3. Edit CloudWatch Logs settings

    • Scroll to the CloudWatch Logs section
    • Click Edit
  4. Enable CloudWatch Logs

    • Check the box to enable CloudWatch Logs
    • For Log group, either:
      • Select an existing log group, or
      • Enter a new name like /aws/cloudtrail/my-trail
    • For IAM role, either:
      • Select an existing role with the right permissions, or
      • Let AWS create a new role (recommended for first-time setup)
  5. Save your changes

    • Click Save changes
    • Wait a few minutes for events to start appearing
AWS CLI (optional)

Step 1: Create a CloudWatch Logs log group

aws logs create-log-group \
--log-group-name /aws/cloudtrail/my-trail \
--region us-east-1

Step 2: Create an IAM role for CloudTrail

First, create a trust policy file:

cat > /tmp/cloudtrail-trust-policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "cloudtrail.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
EOF

Create the role:

aws iam create-role \
--role-name CloudTrail-CloudWatch-Logs-Role \
--assume-role-policy-document file:///tmp/cloudtrail-trust-policy.json \
--region us-east-1

Step 3: Attach permissions to the role

Create a permissions policy (replace <your-account-id> with your AWS account ID):

cat > /tmp/cloudtrail-cwl-policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:us-east-1:<your-account-id>:log-group:/aws/cloudtrail/my-trail:*"
}
]
}
EOF

Attach the policy:

aws iam put-role-policy \
--role-name CloudTrail-CloudWatch-Logs-Role \
--policy-name CloudTrail-CloudWatch-Logs-Policy \
--policy-document file:///tmp/cloudtrail-cwl-policy.json

Step 4: Update the trail

Replace <your-account-id> and <trail-name> with your values:

aws cloudtrail update-trail \
--name <trail-name> \
--cloud-watch-logs-log-group-arn arn:aws:logs:us-east-1:<your-account-id>:log-group:/aws/cloudtrail/my-trail \
--cloud-watch-logs-role-arn arn:aws:iam::<your-account-id>:role/CloudTrail-CloudWatch-Logs-Role \
--region us-east-1
CloudFormation (optional)

This template creates a CloudWatch Logs log group, IAM role, and updates a CloudTrail trail:

AWSTemplateFormatVersion: '2010-09-09'
Description: Enable CloudWatch Logs for CloudTrail

Parameters:
TrailName:
Type: String
Description: Name of the existing CloudTrail trail
Default: my-trail

S3BucketName:
Type: String
Description: S3 bucket where CloudTrail logs are stored

Resources:
CloudTrailLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub /aws/cloudtrail/${TrailName}
RetentionInDays: 90

CloudTrailCloudWatchRole:
Type: AWS::IAM::Role
Properties:
RoleName: CloudTrail-CloudWatch-Logs-Role
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: cloudtrail.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: CloudTrailCloudWatchLogsPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- logs:CreateLogStream
- logs:PutLogEvents
Resource: !GetAtt CloudTrailLogGroup.Arn

CloudTrailWithCloudWatch:
Type: AWS::CloudTrail::Trail
DependsOn:
- CloudTrailLogGroup
- CloudTrailCloudWatchRole
Properties:
TrailName: !Ref TrailName
S3BucketName: !Ref S3BucketName
IsLogging: true
CloudWatchLogsLogGroupArn: !GetAtt CloudTrailLogGroup.Arn
CloudWatchLogsRoleArn: !GetAtt CloudTrailCloudWatchRole.Arn

Outputs:
LogGroupArn:
Description: CloudWatch Logs log group ARN
Value: !GetAtt CloudTrailLogGroup.Arn

RoleArn:
Description: IAM role ARN for CloudTrail
Value: !GetAtt CloudTrailCloudWatchRole.Arn

Deploy with:

aws cloudformation deploy \
--template-file cloudtrail-cloudwatch.yaml \
--stack-name cloudtrail-cloudwatch-logging \
--parameter-overrides TrailName=my-trail S3BucketName=my-cloudtrail-bucket \
--capabilities CAPABILITY_NAMED_IAM \
--region us-east-1
Terraform (optional)
# Variables
variable "trail_name" {
description = "Name of the CloudTrail trail"
type = string
default = "my-trail"
}

variable "s3_bucket_name" {
description = "S3 bucket for CloudTrail logs"
type = string
}

# Data source for current account
data "aws_caller_identity" "current" {}

# CloudWatch Logs log group
resource "aws_cloudwatch_log_group" "cloudtrail" {
name = "/aws/cloudtrail/${var.trail_name}"
retention_in_days = 90
}

# IAM role for CloudTrail to write to CloudWatch Logs
resource "aws_iam_role" "cloudtrail_cloudwatch" {
name = "CloudTrail-CloudWatch-Logs-Role"

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

# IAM policy for the role
resource "aws_iam_role_policy" "cloudtrail_cloudwatch" {
name = "CloudTrail-CloudWatch-Logs-Policy"
role = aws_iam_role.cloudtrail_cloudwatch.id

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

# CloudTrail trail with CloudWatch Logs integration
resource "aws_cloudtrail" "main" {
name = var.trail_name
s3_bucket_name = var.s3_bucket_name
cloud_watch_logs_group_arn = "${aws_cloudwatch_log_group.cloudtrail.arn}:*"
cloud_watch_logs_role_arn = aws_iam_role.cloudtrail_cloudwatch.arn

depends_on = [
aws_iam_role_policy.cloudtrail_cloudwatch
]
}

# Outputs
output "log_group_arn" {
description = "CloudWatch Logs log group ARN"
value = aws_cloudwatch_log_group.cloudtrail.arn
}

output "role_arn" {
description = "IAM role ARN for CloudTrail"
value = aws_iam_role.cloudtrail_cloudwatch.arn
}

Deploy with:

terraform init
terraform plan -var="trail_name=my-trail" -var="s3_bucket_name=my-cloudtrail-bucket"
terraform apply -var="trail_name=my-trail" -var="s3_bucket_name=my-cloudtrail-bucket"

Verification

After making changes, verify the integration is working:

  1. In the AWS Console:

    • Go to CloudTrail > Trails and select your trail
    • Check that the CloudWatch Logs section shows a log group and recent delivery time
    • Go to CloudWatch > Log groups and find your log group
    • Click on it and verify log streams are being created
  2. Generate a test event:

    • Perform any AWS action (like listing S3 buckets)
    • Wait 5-10 minutes
    • Check the CloudWatch log group for new events
CLI verification commands

Check trail configuration:

aws cloudtrail describe-trails \
--trail-name-list <trail-name> \
--region us-east-1 \
--query 'trailList[0].{CloudWatchLogsLogGroupArn:CloudWatchLogsLogGroupArn,CloudWatchLogsRoleArn:CloudWatchLogsRoleArn}'

Check trail status (look for LatestCloudWatchLogsDeliveryTime):

aws cloudtrail get-trail-status \
--name <trail-name> \
--region us-east-1

The output should include a recent LatestCloudWatchLogsDeliveryTime (within the last 24 hours).

Additional Resources

Notes

  • Timing: It can take 5-15 minutes for the first events to appear in CloudWatch Logs after enabling the integration.
  • Costs: CloudWatch Logs incurs charges for ingestion and storage. Consider setting a retention policy to control costs.
  • Multi-region trails: If you have a multi-region trail, you only need to configure CloudWatch Logs once; it applies to all regions.
  • Existing log groups: You can use an existing log group, but ensure the IAM role has permissions to write to it.
  • Log retention: Set an appropriate retention period on the log group to balance cost and compliance requirements.