| name | aws-cloudtrail |
| description | Configure AWS CloudTrail for audit logging. Set up organization trails and event analysis. Use when auditing AWS activity. |
| license | MIT |
| metadata | {"author":"devops-skills","version":"1.0"} |
AWS CloudTrail
Audit AWS account activity with CloudTrail for compliance, security investigation, and operational troubleshooting.
When to Use
- Enabling organization-wide audit logging across all AWS accounts
- Investigating security incidents or unauthorized API activity
- Meeting compliance requirements for SOC 2, HIPAA, PCI DSS, or FedRAMP
- Setting up automated alerting on sensitive AWS API calls
- Querying historical AWS activity for forensic analysis
Create an Organization Trail
aws s3api create-bucket \
--bucket org-cloudtrail-audit-logs \
--region us-east-1
aws s3api put-bucket-policy \
--bucket org-cloudtrail-audit-logs \
--policy '{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AWSCloudTrailAclCheck",
"Effect": "Allow",
"Principal": {"Service": "cloudtrail.amazonaws.com"},
"Action": "s3:GetBucketAcl",
"Resource": "arn:aws:s3:::org-cloudtrail-audit-logs"
},
{
"Sid": "AWSCloudTrailWrite",
"Effect": "Allow",
"Principal": {"Service": "cloudtrail.amazonaws.com"},
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::org-cloudtrail-audit-logs/AWSLogs/*",
"Condition": {
"StringEquals": {"s3:x-amz-acl": "bucket-owner-full-control"}
}
}
]
}'
aws s3api put-public-access-block \
--bucket org-cloudtrail-audit-logs \
--public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
aws s3api put-bucket-versioning \
--bucket org-cloudtrail-audit-logs \
--versioning-configuration Status=Enabled
aws s3api put-bucket-encryption \
--bucket org-cloudtrail-audit-logs \
--server-side-encryption-configuration '{
"Rules": [{"ApplyServerSideEncryptionByDefault": {"SSEAlgorithm": "aws:kms", "KMSMasterKeyID": "alias/cloudtrail-key"}}]
}'
aws s3api put-bucket-lifecycle-configuration \
--bucket org-cloudtrail-audit-logs \
--lifecycle-configuration '{
"Rules": [
{
"ID": "TransitionToGlacier",
"Status": "Enabled",
"Filter": {"Prefix": "AWSLogs/"},
"Transitions": [
{"Days": 90, "StorageClass": "GLACIER"}
]
},
{
"ID": "ExpireOldLogs",
"Status": "Enabled",
"Filter": {"Prefix": "AWSLogs/"},
"Expiration": {"Days": 2555}
}
]
}'
aws cloudtrail create-trail \
--name org-audit-trail \
--s3-bucket-name org-cloudtrail-audit-logs \
--is-organization-trail \
--is-multi-region-trail \
--enable-log-file-validation \
--kms-key-id arn:aws:kms:us-east-1:123456789012:alias/cloudtrail-key \
--cloud-watch-logs-log-group-arn arn:aws:logs:us-east-1:123456789012:log-group:CloudTrail:* \
--cloud-watch-logs-role-arn arn:aws:iam::123456789012:role/CloudTrail-CWLogs-Role
aws cloudtrail start-logging --name org-audit-trail
Event Selectors for Management and Data Events
aws cloudtrail put-event-selectors \
--trail-name org-audit-trail \
--advanced-event-selectors '[
{
"Name": "AllManagementEvents",
"FieldSelectors": [
{"Field": "eventCategory", "Equals": ["Management"]}
]
},
{
"Name": "S3DataEventsForSensitiveBuckets",
"FieldSelectors": [
{"Field": "eventCategory", "Equals": ["Data"]},
{"Field": "resources.type", "Equals": ["AWS::S3::Object"]},
{"Field": "resources.ARN", "StartsWith": [
"arn:aws:s3:::sensitive-data-bucket/",
"arn:aws:s3:::pii-bucket/",
"arn:aws:s3:::financial-data/"
]}
]
},
{
"Name": "LambdaInvocations",
"FieldSelectors": [
{"Field": "eventCategory", "Equals": ["Data"]},
{"Field": "resources.type", "Equals": ["AWS::Lambda::Function"]}
]
},
{
"Name": "DynamoDBDataEvents",
"FieldSelectors": [
{"Field": "eventCategory", "Equals": ["Data"]},
{"Field": "resources.type", "Equals": ["AWS::DynamoDB::Table"]}
]
}
]'
CloudWatch Alerts for Sensitive Activity
aws logs put-metric-filter \
--log-group-name CloudTrail \
--filter-name UnauthorizedAPICalls \
--filter-pattern '{ ($.errorCode = "*UnauthorizedAccess*") || ($.errorCode = "AccessDenied*") }' \
--metric-transformations \
metricName=UnauthorizedAPICalls,metricNamespace=CloudTrailMetrics,metricValue=1
aws cloudwatch put-metric-alarm \
--alarm-name UnauthorizedAPICallsAlarm \
--metric-name UnauthorizedAPICalls \
--namespace CloudTrailMetrics \
--statistic Sum \
--period 300 \
--threshold 5 \
--comparison-operator GreaterThanOrEqualToThreshold \
--evaluation-periods 1 \
--alarm-actions arn:aws:sns:us-east-1:123456789012:security-alerts
aws logs put-metric-filter \
--log-group-name CloudTrail \
--filter-name RootAccountUsage \
--filter-pattern '{ ($.userIdentity.type = "Root") && ($.userIdentity.invokedBy NOT EXISTS) && ($.eventType != "AwsServiceEvent") }' \
--metric-transformations \
metricName=RootAccountUsage,metricNamespace=CloudTrailMetrics,metricValue=1
aws cloudwatch put-metric-alarm \
--alarm-name RootAccountUsageAlarm \
--metric-name RootAccountUsage \
--namespace CloudTrailMetrics \
--statistic Sum \
--period 300 \
--threshold 1 \
--comparison-operator GreaterThanOrEqualToThreshold \
--evaluation-periods 1 \
--alarm-actions arn:aws:sns:us-east-1:123456789012:security-alerts
aws logs put-metric-filter \
--log-group-name CloudTrail \
--filter-name ConsoleLoginWithoutMFA \
--filter-pattern '{ ($.eventName = "ConsoleLogin") && ($.additionalEventData.MFAUsed != "Yes") && ($.userIdentity.type = "IAMUser") }' \
--metric-transformations \
metricName=ConsoleLoginWithoutMFA,metricNamespace=CloudTrailMetrics,metricValue=1
aws logs put-metric-filter \
--log-group-name CloudTrail \
--filter-name IAMPolicyChanges \
--filter-pattern '{ ($.eventName=CreatePolicy) || ($.eventName=DeletePolicy) || ($.eventName=AttachRolePolicy) || ($.eventName=DetachRolePolicy) || ($.eventName=AttachUserPolicy) || ($.eventName=PutUserPolicy) }' \
--metric-transformations \
metricName=IAMPolicyChanges,metricNamespace=CloudTrailMetrics,metricValue=1
aws logs put-metric-filter \
--log-group-name CloudTrail \
--filter-name SecurityGroupChanges \
--filter-pattern '{ ($.eventName=AuthorizeSecurityGroupIngress) || ($.eventName=RevokeSecurityGroupIngress) || ($.eventName=CreateSecurityGroup) || ($.eventName=DeleteSecurityGroup) }' \
--metric-transformations \
metricName=SecurityGroupChanges,metricNamespace=CloudTrailMetrics,metricValue=1
Athena Queries for CloudTrail Analysis
CREATE EXTERNAL TABLE IF NOT EXISTS cloudtrail_logs (
eventVersion STRING,
userIdentity STRUCT<
type: STRING,
principalId: STRING,
arn: STRING,
accountId: STRING,
invokedBy: STRING,
accessKeyId: STRING,
userName: STRING,
sessionContext: STRUCT<
attributes: STRUCT<mfaAuthenticated: STRING, creationDate: STRING>,
sessionIssuer: STRUCT<type: STRING, principalId: STRING, arn: STRING, accountId: STRING, userName: STRING>
>
>,
eventTime STRING,
eventSource STRING,
eventName STRING,
awsRegion STRING,
sourceIPAddress STRING,
userAgent STRING,
errorCode STRING,
errorMessage STRING,
requestParameters STRING,
responseElements STRING,
additionalEventData STRING,
requestId STRING,
eventId STRING,
readOnly STRING,
resources ARRAY<STRUCT<arn: STRING, accountId: STRING, type: STRING>>,
eventType STRING,
recipientAccountId STRING
)
PARTITIONED BY (region STRING, year STRING, month STRING, day STRING)
ROW FORMAT SERDE 'org.apache.hive.hcatalog.data.JsonSerDe'
LOCATION 's3://org-cloudtrail-audit-logs/AWSLogs/123456789012/CloudTrail/';
SELECT eventTime, userIdentity.arn, eventName, sourceIPAddress,
requestParameters
FROM cloudtrail_logs
WHERE eventName LIKE '%Delete%'
AND eventTime > date_format(date_add('day', -7, current_date), )
eventTime
LIMIT ;
eventTime, userIdentity.userName, sourceIPAddress,
additionalEventData
cloudtrail_logs
eventName
sourceIPAddress (, )
eventTime date_format(date_add(, , ), )
eventTime ;
userIdentity.arn,
() api_call_count,
( eventName) unique_actions,
( sourceIPAddress) unique_ips,
(eventTime) first_seen,
(eventTime) last_seen
cloudtrail_logs
eventTime date_format(date_add(, , ), )
userIdentity.arn
api_call_count
LIMIT ;
eventTime, userIdentity.arn, eventName, errorCode, errorMessage,
sourceIPAddress
cloudtrail_logs
errorCode (, , )
eventTime date_format(date_add(, , ), )
eventTime
LIMIT ;
eventTime, userIdentity.arn, eventName, requestParameters,
resources[].arn key_arn
cloudtrail_logs
eventSource
eventName (, , , , )
eventTime date_format(date_add(, , ), )
eventTime ;
CloudTrail Lake (Event Data Store)
aws cloudtrail create-event-data-store \
--name org-audit-event-store \
--multi-region-enabled \
--organization-enabled \
--retention-period 2555 \
--advanced-event-selectors '[
{
"Name": "AllManagementEvents",
"FieldSelectors": [
{"Field": "eventCategory", "Equals": ["Management"]}
]
}
]'
SELECT eventTime, eventName, eventSource, sourceIPAddress,
errorCode, requestParameters
FROM EVENT_DATA_STORE_ID
WHERE userIdentity.arn = 'arn:aws:iam::123456789012:user/suspicious-user'
AND eventTime > '2024-01-01 00:00:00'
ORDER BY eventTime DESC;
SELECT recipientAccountId, userIdentity.arn,
count(*) AS event_count
FROM EVENT_DATA_STORE_ID
WHERE eventTime > '2024-01-01 00:00:00'
GROUP BY recipientAccountId, userIdentity.arn
ORDER BY event_count DESC;
Validate Trail Integrity
aws cloudtrail validate-logs \
--trail-arn arn:aws:cloudtrail:us-east-1:123456789012:trail/org-audit-trail \
--start-time "2024-01-01T00:00:00Z" \
--end-time "2024-01-31T23:59:59Z"
aws cloudtrail get-trail-status --name org-audit-trail
aws cloudtrail describe-trails --trail-name-list org-audit-trail
Terraform Configuration
resource "aws_cloudtrail" "org_trail" {
name = "org-audit-trail"
s3_bucket_name = aws_s3_bucket.cloudtrail.id
is_organization_trail = true
is_multi_region_trail = true
enable_log_file_validation = true
kms_key_id = aws_kms_key.cloudtrail.arn
cloud_watch_logs_group_arn = "${aws_cloudwatch_log_group.cloudtrail.arn}:*"
cloud_watch_logs_role_arn = aws_iam_role.cloudtrail_cw.arn
include_global_service_events = true
advanced_event_selector {
name = "AllManagementEvents"
field_selector {
field = "eventCategory"
equals = ["Management"]
}
}
advanced_event_selector {
name = "SensitiveS3DataEvents"
field_selector {
field = "eventCategory"
equals = ["Data"]
}
field_selector {
field = "resources.type"
equals = ["AWS::S3::Object"]
}
field_selector {
field = "resources.ARN"
starts_with = ["arn:aws:s3:::sensitive-data-bucket/"]
}
}
tags = {
Environment = "production"
Compliance = "soc2,hipaa"
}
}
Setup Checklist
cloudtrail_checklist:
trail_configuration:
- [ ] Organization trail enabled across all accounts
- [ ] Multi-region trail enabled
- [ ] Log file validation enabled
- [ ] KMS encryption configured with dedicated key
- [ ] CloudWatch Logs integration active
- [ ] S3 bucket policy restricts access to CloudTrail service only
s3_bucket_hardening:
- [ ] Public access blocked
- [ ] Versioning enabled
- [ ] Server-side encryption enabled
- [ ] Lifecycle policy set for
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
Best Practices
- Enable organization-wide trails from the management account for full coverage
- Always enable log file validation to detect tampering
- Encrypt logs with a customer-managed KMS key and restrict key usage
- Use advanced event selectors to capture data events on sensitive resources without logging everything
- Integrate with CloudWatch Logs for real-time metric filters and alarms
- Set up Athena or CloudTrail Lake for efficient querying during investigations
- Apply S3 lifecycle policies to transition old logs to Glacier and enforce retention
- Monitor the trail itself (delivery errors, configuration changes) as a meta-control
- Validate log integrity periodically as part of compliance evidence collection
- Restrict access to the CloudTrail S3 bucket and KMS key with least-privilege IAM policies