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:UpdateTrailcloudtrail:DescribeTrailslogs:CreateLogGrouplogs:DescribeLogGroupsiam:CreateRoleiam:PutRolePolicyiam:PassRole
AWS Console Method
-
Open CloudTrail in the AWS Console
- Go to CloudTrail Console in us-east-1
-
Select your trail
- Click Trails in the left sidebar
- Click on the trail name you want to configure
-
Edit CloudWatch Logs settings
- Scroll to the CloudWatch Logs section
- Click Edit
-
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)
-
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:
-
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
-
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
- AWS Documentation: Sending CloudTrail Events to CloudWatch Logs
- AWS Documentation: CloudWatch Logs Insights
- AWS Documentation: Creating CloudWatch Alarms for CloudTrail Events
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.