AWS CloudTrail: Complete Guide to API Auditing and Compliance (2026)
Every API call made in your AWS account — who ran it, from where, at what time, and what changed — is recorded by AWS CloudTrail. CloudTrail is the backbone of cloud security, compliance, and forensic investigation. This guide covers everything: event types, trail configuration, querying with Athena, anomaly detection with CloudTrail Insights, real-time alerting, EventBridge automation, the new CloudTrail Lake, log integrity validation, and practical compliance mapping for PCI-DSS, SOC2, and HIPAA.
Table of Contents
- 1. CloudTrail Event Types
- 2. Trail Setup — Single-Region, Multi-Region, Org Trails
- 3. CloudTrail Event Structure — Anatomy of a Log
- 4. Querying with Athena — Partition Projection + 10 SQL Queries
- 5. CloudTrail Insights — Anomaly Detection
- 6. Real-Time Alerting with CloudWatch Logs + Metric Filters
- 7. EventBridge Integration — Security Automation
- 8. CloudTrail Lake — Event Data Store and SQL Queries
- 9. Log Integrity Validation
- 10. Compliance Use Cases — PCI-DSS, SOC2, HIPAA
- FAQ
1. CloudTrail Event Types
CloudTrail captures three distinct event categories, each serving a different audit purpose. Understanding the difference determines what you enable, at what cost, and what forensic questions each answers.
Management Events (Control Plane)
Management events record API operations that manage your AWS resources — creating, modifying, or deleting them. These are enabled by default for every trail and cover the most critical security operations:
- Write events:
RunInstances,CreateBucket,PutBucketPolicy,DeleteSecurityGroup,AttachRolePolicy,CreateUser - Read events:
DescribeInstances,ListBuckets,GetCallerIdentity— high volume; most teams filter these out to reduce cost
Management events are free for the first copy in a region. Additional copies of management events (e.g., for a second trail) cost $2.00 per 100,000 events. For most teams, management events alone provide 80% of security value — prioritise enabling write-only management events across all regions before anything else.
Data Events (Data Plane)
Data events record individual object-level operations inside services — the high-volume, low-level API calls inside S3 buckets, Lambda functions, DynamoDB tables, and more:
- S3 object-level:
GetObject,PutObject,DeleteObject— vital for detecting data exfiltration or unauthorised reads of sensitive buckets - Lambda function invocations:
Invoke— track every Lambda execution with caller identity - DynamoDB item-level operations:
PutItem,DeleteItem,GetItem - SQS message operations:
SendMessage,ReceiveMessage,DeleteMessage
Data events are priced at $0.10 per 100,000 events — they can generate enormous volumes for busy S3 buckets. Use advanced event selectors to filter by bucket ARN or prefix to control cost.
CloudTrail Insights Events
Insights events are generated by CloudTrail's ML-based anomaly detection engine. CloudTrail learns your account's baseline API call rate over a 7-day rolling window and fires an Insights event when it detects a statistically significant deviation — either an unusual spike or an unexpected drop. Insights events cover:
- API call rate anomalies: sudden burst of
DescribeInstancescalls could indicate reconnaissance; spike inCreateNetworkInterfacecould mean cryptominer deployment - API error rate anomalies: surge in
AccessDeniederrors often signals an attacker probing permissions or compromised credentials trying to escalate
Insights events are charged at $0.35 per 100,000 events analysed. They are invaluable for catching attacks that blend in by calling legitimate APIs but at abnormal rates.
2. Trail Setup — Single-Region, Multi-Region, and Org Trails
A trail is the CloudTrail configuration that defines what events to capture and where to deliver them. Choosing the right trail architecture is the first decision every security engineer must make.
Single-Region Trail
Captures events only in the region where the trail is created. Useful only for region-specific compliance requirements or debugging. Do not rely on single-region trails for security — attackers often pivot to unexpected regions.
Multi-Region Trail (Recommended)
A multi-region trail automatically applies to all current and future AWS regions. This is the production-recommended configuration:
aws cloudtrail create-trail \
--name "techoral-security-trail" \
--s3-bucket-name "techoral-cloudtrail-logs-prod" \
--include-global-service-events \
--is-multi-region-trail \
--enable-log-file-validation \
--cloud-watch-logs-log-group-arn "arn:aws:logs:us-east-1:123456789012:log-group:CloudTrailLogs:*" \
--cloud-watch-logs-role-arn "arn:aws:iam::123456789012:role/CloudTrailCWLogsRole"
# Start logging
aws cloudtrail start-logging --name "techoral-security-trail"
# Verify status
aws cloudtrail get-trail-status --name "techoral-security-trail"
Organization Trail (AWS Organizations)
An organization trail covers all accounts in your AWS Organization from a single management account — the gold standard for enterprise CloudTrail governance:
aws cloudtrail create-trail \
--name "org-security-trail" \
--s3-bucket-name "org-cloudtrail-central-bucket" \
--is-multi-region-trail \
--is-organization-trail \
--enable-log-file-validation \
--region us-east-1
Terraform Configuration
Infrastructure-as-code is essential for repeatable trail deployment across environments:
resource "aws_cloudtrail" "security_trail" {
name = "techoral-security-trail"
s3_bucket_name = aws_s3_bucket.cloudtrail_bucket.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.cloudtrail.arn}:*"
cloud_watch_logs_role_arn = aws_iam_role.cloudtrail_cw_role.arn
event_selector {
read_write_type = "WriteOnly"
include_management_events = true
data_resource {
type = "AWS::S3::Object"
values = ["arn:aws:s3:::techoral-production-sensitive-bucket/"]
}
}
advanced_event_selector {
name = "Log Lambda invocations for production functions"
field_selector {
field = "eventCategory"
equals = ["Data"]
}
field_selector {
field = "resources.type"
equals = ["AWS::Lambda::Function"]
}
field_selector {
field = "resources.ARN"
starts_with = ["arn:aws:lambda:us-east-1:123456789012:function:prod-"]
}
}
tags = {
Environment = "production"
Purpose = "security-audit"
}
}
resource "aws_s3_bucket_policy" "cloudtrail_bucket_policy" {
bucket = aws_s3_bucket.cloudtrail_bucket.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AWSCloudTrailAclCheck"
Effect = "Allow"
Principal = { Service = "cloudtrail.amazonaws.com" }
Action = "s3:GetBucketAcl"
Resource = aws_s3_bucket.cloudtrail_bucket.arn
},
{
Sid = "AWSCloudTrailWrite"
Effect = "Allow"
Principal = { Service = "cloudtrail.amazonaws.com" }
Action = "s3:PutObject"
Resource = "${aws_s3_bucket.cloudtrail_bucket.arn}/AWSLogs/*"
Condition = {
StringEquals = { "s3:x-amz-acl" = "bucket-owner-full-control" }
}
}
]
})
}
s3:DeleteObject to everyone except a dedicated break-glass role. This prevents log tampering and is required for PCI-DSS and SOC2.3. CloudTrail Event Structure — Anatomy of a Log
Every CloudTrail event is a JSON record delivered to S3 as a gzip-compressed batch file. Understanding the event structure is essential for writing effective Athena queries and alert rules. Here is a real CloudTrail event for an IAM policy attachment:
{
"eventVersion": "1.09",
"userIdentity": {
"type": "IAMUser",
"principalId": "AIDABC123DEF456GHI789",
"arn": "arn:aws:iam::123456789012:user/john.doe",
"accountId": "123456789012",
"accessKeyId": "AKIAIOSFODNN7EXAMPLE",
"userName": "john.doe",
"sessionContext": {
"sessionIssuer": {},
"webIdFederationData": {},
"attributes": {
"creationDate": "2026-06-08T09:00:00Z",
"mfaAuthenticated": "true"
}
}
},
"eventTime": "2026-06-08T14:32:15Z",
"eventSource": "iam.amazonaws.com",
"eventName": "AttachRolePolicy",
"awsRegion": "us-east-1",
"sourceIPAddress": "203.0.113.42",
"userAgent": "aws-cli/2.15.0 Python/3.12.0 Linux/6.1 botocore/2.15.0",
"requestParameters": {
"roleName": "EC2ProductionRole",
"policyArn": "arn:aws:iam::aws:policy/AdministratorAccess"
},
"responseElements": null,
"requestID": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"eventID": "b1c2d3e4-f5a6-7890-bcde-f12345678901",
"readOnly": false,
"resources": [
{
"ARN": "arn:aws:iam::123456789012:role/EC2ProductionRole",
"accountId": "123456789012",
"type": "AWS::IAM::Role"
}
],
"eventType": "AwsApiCall",
"managementEvent": true,
"recipientAccountId": "123456789012",
"eventCategory": "Management"
}
Key fields every security engineer must know:
- userIdentity.type:
Root,IAMUser,AssumedRole,FederatedUser,AWSService,AWSAccount— the type determines how to interpret the identity - userIdentity.sessionContext.attributes.mfaAuthenticated:
"true"or"false"— critical for detecting console access without MFA - eventName: the API action called — this is your primary filter in every query
- sourceIPAddress: the IP of the caller; for AWS services it shows the service name (e.g.,
ec2.amazonaws.com) - requestParameters: what was requested — ARNs, resource names, policy documents
- errorCode / errorMessage: present only when the API call failed — key for detecting access denials and probing
- readOnly:
truefor read operations,falsefor mutating calls — allows quick filtering to write events only
4. Querying with Athena — Partition Projection + 10 SQL Queries
CloudTrail logs stored in S3 can be queried directly with Amazon Athena. The key performance optimization is partition projection — Athena automatically computes partition values from the S3 key pattern instead of maintaining a Glue Data Catalog partition, reducing query setup time from minutes to seconds.
Create the Athena Table with Partition Projection
CREATE EXTERNAL TABLE 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,
resources ARRAY<STRUCT<arn:STRING,accountid:STRING,type:STRING>>,
eventtype STRING,
apiversion STRING,
readonly STRING,
recipientaccountid STRING,
serviceeventdetails STRING,
sharedeventid STRING,
vpcendpointid STRING,
managementevent STRING,
eventcategory STRING
)
PARTITIONED BY (
region STRING,
year STRING,
month STRING,
day STRING
)
ROW FORMAT SERDE 'com.amazon.emr.hive.serde.CloudTrailSerde'
STORED AS INPUTFORMAT 'com.amazon.emr.cloudtrail.CloudTrailInputFormat'
OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
LOCATION 's3://techoral-cloudtrail-logs-prod/AWSLogs/123456789012/CloudTrail/'
TBLPROPERTIES (
'projection.enabled' = 'true',
'projection.region.type' = 'enum',
'projection.region.values' = 'us-east-1,us-west-2,eu-west-1,ap-southeast-1',
'projection.year.type' = 'integer',
'projection.year.range' = '2023,2030',
'projection.month.type' = 'integer',
'projection.month.range' = '1,12',
'projection.month.digits' = '2',
'projection.day.type' = 'integer',
'projection.day.range' = '1,31',
'projection.day.digits' = '2',
'storage.location.template' = 's3://techoral-cloudtrail-logs-prod/AWSLogs/123456789012/CloudTrail/${region}/${year}/${month}/${day}/'
);
10 Real-World Athena Queries
Query 1: Who deleted what in the last 7 days?
SELECT
eventtime,
useridentity.arn AS caller_arn,
useridentity.username AS username,
eventname,
awsregion,
sourceipaddress,
requestparameters
FROM cloudtrail_logs
WHERE year = '2026'
AND month IN ('05','06')
AND eventname LIKE 'Delete%'
AND eventtime >= date_format(current_timestamp - interval '7' day, '%Y-%m-%dT%H:%i:%sZ')
ORDER BY eventtime DESC
LIMIT 500;
Query 2: Root account usage (any action by root)
SELECT
eventtime,
eventname,
eventsource,
sourceipaddress,
awsregion,
useridentity.sessioncontext.attributes.mfaauthenticated AS mfa
FROM cloudtrail_logs
WHERE year = '2026'
AND useridentity.type = 'Root'
AND eventname != 'AwsServiceEvent'
ORDER BY eventtime DESC;
Query 3: Console logins without MFA
SELECT
eventtime,
useridentity.username AS username,
sourceipaddress,
awsregion,
responseelements
FROM cloudtrail_logs
WHERE year = '2026'
AND eventname = 'ConsoleLogin'
AND json_extract_scalar(responseelements, '$.ConsoleLogin') = 'Success'
AND useridentity.sessioncontext.attributes.mfaauthenticated = 'false'
ORDER BY eventtime DESC;
Query 4: IAM privilege escalation — AdministratorAccess policy attached
SELECT
eventtime,
useridentity.arn AS actor,
eventname,
json_extract_scalar(requestparameters, '$.policyArn') AS policy_arn,
json_extract_scalar(requestparameters, '$.roleName') AS role_name,
json_extract_scalar(requestparameters, '$.userName') AS target_user
FROM cloudtrail_logs
WHERE year = '2026'
AND eventname IN ('AttachRolePolicy','AttachUserPolicy','AttachGroupPolicy')
AND requestparameters LIKE '%AdministratorAccess%'
ORDER BY eventtime DESC;
Query 5: Unusual login times (logins between midnight and 6am UTC)
SELECT
eventtime,
useridentity.username AS username,
sourceipaddress,
awsregion
FROM cloudtrail_logs
WHERE year = '2026'
AND eventname = 'ConsoleLogin'
AND json_extract_scalar(responseelements, '$.ConsoleLogin') = 'Success'
AND cast(substr(eventtime, 12, 2) AS integer) BETWEEN 0 AND 5
ORDER BY eventtime DESC;
Query 6: Security group changes — open-to-world rules added
SELECT
eventtime,
useridentity.arn AS actor,
eventname,
sourceipaddress,
requestparameters
FROM cloudtrail_logs
WHERE year = '2026'
AND eventname IN ('AuthorizeSecurityGroupIngress','AuthorizeSecurityGroupEgress')
AND (requestparameters LIKE '%0.0.0.0/0%' OR requestparameters LIKE '%::/0%')
ORDER BY eventtime DESC;
Query 7: S3 bucket policy changes
SELECT
eventtime,
useridentity.arn AS actor,
eventname,
awsregion,
json_extract_scalar(requestparameters,'$.bucketName') AS bucket_name
FROM cloudtrail_logs
WHERE year = '2026'
AND eventsource = 's3.amazonaws.com'
AND eventname IN ('PutBucketPolicy','DeleteBucketPolicy','PutBucketAcl','PutBucketPublicAccessBlock')
ORDER BY eventtime DESC;
Query 8: Failed API calls (access denied) — potential attacker probing
SELECT
useridentity.arn AS caller,
sourceipaddress,
eventname,
count(*) AS denial_count
FROM cloudtrail_logs
WHERE year = '2026'
AND month = '06'
AND errorcode = 'AccessDenied'
GROUP BY useridentity.arn, sourceipaddress, eventname
HAVING count(*) > 10
ORDER BY denial_count DESC
LIMIT 100;
Query 9: KMS key deletion or disable (potential ransomware)
SELECT
eventtime,
useridentity.arn AS actor,
eventname,
sourceipaddress,
json_extract_scalar(requestparameters,'$.keyId') AS key_id
FROM cloudtrail_logs
WHERE year = '2026'
AND eventsource = 'kms.amazonaws.com'
AND eventname IN ('DisableKey','ScheduleKeyDeletion','DeleteImportedKeyMaterial')
ORDER BY eventtime DESC;
Query 10: New IAM users or access keys created
SELECT
eventtime,
useridentity.arn AS creator,
eventname,
json_extract_scalar(requestparameters,'$.userName') AS new_username,
json_extract_scalar(responseelements,'$.accessKey.accessKeyId') AS new_key_id,
sourceipaddress
FROM cloudtrail_logs
WHERE year = '2026'
AND eventname IN ('CreateUser','CreateAccessKey','CreateLoginProfile')
ORDER BY eventtime DESC;
year, month, and day partition columns first in your WHERE clause. Athena charges per GB scanned — a query without partition filters can scan years of logs costing hundreds of dollars. Partition projection eliminates the need to run MSCK REPAIR TABLE as new partitions appear daily.5. CloudTrail Insights — Anomaly Detection
CloudTrail Insights is the built-in ML anomaly detection layer. It operates entirely without configuration: once enabled, it monitors your API call rate and error rate, builds a rolling 7-day baseline, and emits an Insights event whenever it detects a statistically unusual deviation. This catches threats that evade rule-based detections because the API calls themselves are legitimate — it's the volume that's anomalous.
Enabling Insights on an Existing Trail
aws cloudtrail put-insight-selectors \
--trail-name "techoral-security-trail" \
--insight-selectors \
'[{"InsightType":"ApiCallRateInsight"},{"InsightType":"ApiErrorRateInsight"}]'
# Verify
aws cloudtrail get-insight-selectors --trail-name "techoral-security-trail"
Anatomy of a CloudTrail Insights Event
{
"eventVersion": "1.08",
"eventTime": "2026-06-08T16:45:00Z",
"eventName": "DescribeInstances",
"eventSource": "ec2.amazonaws.com",
"eventCategory": "Insight",
"insightDetails": {
"state": "Start",
"eventSource": "ec2.amazonaws.com",
"eventName": "DescribeInstances",
"insightType": "ApiCallRateInsight",
"insightContext": {
"statistics": {
"baseline": {
"average": 1.4
},
"insight": {
"average": 247.3
},
"insightDuration": 2,
"baselineDuration": 7
}
}
}
}
The insightDetails.insightContext.statistics block tells the full story: the baseline average was 1.4 calls per minute; during the insight window it spiked to 247.3 calls per minute. This specific pattern — a massive spike in DescribeInstances — is a classic reconnaissance signature. Common high-value Insights detections:
- Spike in
GetObjecton an S3 bucket — potential data exfiltration - Surge in
CreateNetworkInterface— cryptominer trying to reach out - Burst of
AssumeRolecalls — lateral movement using role chaining - Spike in
AccessDeniederrors from a single IP — attacker probing permissions
6. Real-Time Alerting with CloudWatch Logs + Metric Filters
CloudTrail can stream events to CloudWatch Logs in near-real-time (typically under 15 minutes). From there, CloudWatch metric filters can turn specific event patterns into numeric metrics, and CloudWatch Alarms can page your on-call team the moment a critical event is detected.
CloudWatch Logs IAM Role for CloudTrail
aws iam create-role \
--role-name CloudTrailCWLogsRole \
--assume-role-policy-document '{
"Version":"2012-10-17",
"Statement":[{
"Effect":"Allow",
"Principal":{"Service":"cloudtrail.amazonaws.com"},
"Action":"sts:AssumeRole"
}]
}'
aws iam put-role-policy \
--role-name CloudTrailCWLogsRole \
--policy-name CWLogsPolicy \
--policy-document '{
"Version":"2012-10-17",
"Statement":[{
"Effect":"Allow",
"Action":["logs:CreateLogStream","logs:PutLogEvents"],
"Resource":"arn:aws:logs:us-east-1:123456789012:log-group:CloudTrailLogs:*"
}]
}'
Metric Filter — Root Account Usage
aws logs put-metric-filter \
--log-group-name "CloudTrailLogs" \
--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 "RootAccountUsage" \
--alarm-description "Root account was used — investigate immediately" \
--namespace CloudTrailMetrics \
--metric-name RootAccountUsage \
--statistic Sum \
--period 300 \
--evaluation-periods 1 \
--threshold 1 \
--comparison-operator GreaterThanOrEqualToThreshold \
--alarm-actions "arn:aws:sns:us-east-1:123456789012:security-alerts" \
--treat-missing-data notBreaching
Metric Filter — Security Group Changes
aws logs put-metric-filter \
--log-group-name "CloudTrailLogs" \
--filter-name "SecurityGroupChanges" \
--filter-pattern '{ ($.eventName = AuthorizeSecurityGroupIngress) || ($.eventName = AuthorizeSecurityGroupEgress) || ($.eventName = RevokeSecurityGroupIngress) || ($.eventName = RevokeSecurityGroupEgress) || ($.eventName = CreateSecurityGroup) || ($.eventName = DeleteSecurityGroup) }' \
--metric-transformations \
metricName=SecurityGroupChanges,metricNamespace=CloudTrailMetrics,metricValue=1
Metric Filter — S3 Bucket Policy Changes
aws logs put-metric-filter \
--log-group-name "CloudTrailLogs" \
--filter-name "S3BucketPolicyChanges" \
--filter-pattern '{ ($.eventSource = s3.amazonaws.com) && (($.eventName = PutBucketAcl) || ($.eventName = PutBucketPolicy) || ($.eventName = PutBucketCors) || ($.eventName = PutBucketLifecycle) || ($.eventName = PutBucketReplication) || ($.eventName = DeleteBucketPolicy) || ($.eventName = DeleteBucketCors) || ($.eventName = DeleteBucketLifecycle) || ($.eventName = DeleteBucketReplication)) }' \
--metric-transformations \
metricName=S3BucketPolicyChanges,metricNamespace=CloudTrailMetrics,metricValue=1
Metric Filter — Console Login Without MFA
aws logs put-metric-filter \
--log-group-name "CloudTrailLogs" \
--filter-name "ConsoleLoginWithoutMFA" \
--filter-pattern '{ ($.eventName = ConsoleLogin) && ($.additionalEventData.MFAUsed != "Yes") && ($.userIdentity.type != "AssumedRole") }' \
--metric-transformations \
metricName=ConsoleLoginWithoutMFA,metricNamespace=CloudTrailMetrics,metricValue=1
7. EventBridge Integration — Real-Time Security Automation
CloudTrail can route events to Amazon EventBridge, enabling sub-minute automated security responses. Unlike CloudWatch Logs metric filters (which batch events and add latency), EventBridge receives CloudTrail management events in near-real-time. This is the architecture for automated incident response.
EventBridge Rule for IAM Changes
aws events put-rule \
--name "CloudTrail-IAM-Changes" \
--event-pattern '{
"source": ["aws.iam"],
"detail-type": ["AWS API Call via CloudTrail"],
"detail": {
"eventSource": ["iam.amazonaws.com"],
"eventName": [
"CreateUser","DeleteUser","AttachUserPolicy","DetachUserPolicy",
"AttachRolePolicy","DetachRolePolicy","CreateAccessKey",
"UpdateAssumeRolePolicy"
]
}
}' \
--state ENABLED
aws events put-targets \
--rule "CloudTrail-IAM-Changes" \
--targets '[{
"Id": "SecurityLambda",
"Arn": "arn:aws:lambda:us-east-1:123456789012:function:security-responder"
}]'
Python Lambda Responder — Auto-Quarantine Suspicious IAM User
import boto3
import json
import os
from datetime import datetime
iam = boto3.client('iam')
sns = boto3.client('sns')
ssm = boto3.client('ssm')
ALERT_TOPIC = os.environ['SECURITY_SNS_TOPIC_ARN']
TRUSTED_ROLES = os.environ.get('TRUSTED_ROLES', '').split(',')
def lambda_handler(event, context):
detail = event.get('detail', {})
event_name = detail.get('eventName', '')
user_id = detail.get('userIdentity', {})
actor_arn = user_id.get('arn', 'unknown')
event_time = detail.get('eventTime', datetime.utcnow().isoformat())
# Skip trusted automation roles
if any(role in actor_arn for role in TRUSTED_ROLES if role):
print(f"Skipping trusted role: {actor_arn}")
return {'statusCode': 200, 'body': 'Skipped trusted role'}
# High-severity: AdministratorAccess attached to a user or role
request_params = detail.get('requestParameters', {}) or {}
policy_arn = request_params.get('policyArn', '')
target_user = request_params.get('userName', '')
target_role = request_params.get('roleName', '')
if 'AdministratorAccess' in policy_arn:
severity = 'CRITICAL'
action_taken = _quarantine_user_if_applicable(
event_name, target_user, actor_arn
)
else:
severity = 'HIGH'
action_taken = 'Logged — no automated action'
# Post alert
message = {
'severity': severity,
'event_name': event_name,
'actor_arn': actor_arn,
'target_user': target_user,
'target_role': target_role,
'policy_arn': policy_arn,
'event_time': event_time,
'action_taken': action_taken
}
sns.publish(
TopicArn = ALERT_TOPIC,
Subject = f'[{severity}] CloudTrail Security Alert: {event_name}',
Message = json.dumps(message, indent=2)
)
# Log to SSM Parameter Store for audit trail
ssm.put_parameter(
Name = f'/security/incidents/{context.aws_request_id}',
Value = json.dumps(message),
Type = 'String',
Overwrite = False
)
return {'statusCode': 200, 'body': json.dumps(message)}
def _quarantine_user_if_applicable(event_name, username, actor_arn):
"""Detach AdministratorAccess and add deny-all boundary if actor is not an admin pipeline."""
if not username or 'pipeline' in actor_arn.lower() or 'automation' in actor_arn.lower():
return 'Skipped auto-quarantine — pipeline actor or no username'
try:
# Detach the offending policy
iam.detach_user_policy(
UserName = username,
PolicyArn = 'arn:aws:iam::aws:policy/AdministratorAccess'
)
# Apply deny-all permissions boundary
iam.put_user_permissions_boundary(
UserName = username,
PermissionsBoundary = 'arn:aws:iam::aws:policy/AWSDenyAll'
)
return f'Auto-quarantined: detached AdministratorAccess and applied DenyAll boundary to {username}'
except iam.exceptions.NoSuchEntityException:
return f'User {username} not found — may have already been removed'
except Exception as exc:
return f'Auto-quarantine failed: {str(exc)}'
8. CloudTrail Lake — Event Data Store and Federated SQL
CloudTrail Lake is a managed, immutable event data store that keeps CloudTrail events in a purpose-built query engine — no S3, no Athena table management, no partition projection setup. Events are stored for up to 7 years (default: 7 years at standard tier, up to 10 years at long-term retention). The query language is standard SQL.
Create an Event Data Store
aws cloudtrail create-event-data-store \
--name "techoral-security-lake" \
--retention-period 2557 \
--multi-region-enabled \
--organization-enabled \
--termination-protection-enabled \
--advanced-event-selectors '[{
"Name": "All management events",
"FieldSelectors": [
{"Field": "eventCategory", "Equals": ["Management"]}
]
}]'
CloudTrail Lake SQL Queries
CloudTrail Lake uses its own query console with a SQL dialect similar to ANSI SQL. Event data is queried directly — no CREATE TABLE needed:
-- Who made IAM changes in the last 30 days?
SELECT
eventTime,
userIdentity.arn AS caller,
eventName,
requestParameters
FROM eds-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
WHERE eventTime BETWEEN '2026-05-09 00:00:00' AND '2026-06-08 23:59:59'
AND eventSource = 'iam.amazonaws.com'
AND readOnly = false
ORDER BY eventTime DESC
LIMIT 200;
-- Top 10 most active API callers this month
SELECT
userIdentity.arn AS caller,
count(*) AS api_call_count
FROM eds-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
WHERE eventTime BETWEEN '2026-06-01 00:00:00' AND '2026-06-08 23:59:59'
GROUP BY userIdentity.arn
ORDER BY api_call_count DESC
LIMIT 10;
CloudTrail Lake vs Athena — When to Use Which
| Feature | CloudTrail Lake | Athena + S3 |
|---|---|---|
| Setup complexity | Low — managed store | Medium — S3 bucket, table DDL, partitions |
| Query speed | Fast — purpose-built | Good with partition projection |
| Retention | Up to 10 years managed | Unlimited — you manage S3 lifecycle |
| Cost model | Per GB ingested + scanned | S3 storage + $5/TB scanned |
| Cross-account | Native org trail support | Manual bucket aggregation |
| Best for | Compliance, long-term SIEM | Custom analytics, existing S3 pipelines |
9. Log Integrity Validation
CloudTrail log file integrity validation ensures that log files have not been tampered with, deleted, or modified after delivery to S3. When enabled, CloudTrail delivers a signed digest file every hour — the digest contains the SHA-256 hash of each log file delivered in that hour, and each digest is signed with the CloudTrail private key (you validate with the public key embedded in the AWS certificate).
Enable Log File Validation (must be set at trail creation or update)
# Enable on existing trail
aws cloudtrail update-trail \
--name "techoral-security-trail" \
--enable-log-file-validation
# Validate logs for the last 24 hours
aws cloudtrail validate-logs \
--trail-arn "arn:aws:cloudtrail:us-east-1:123456789012:trail/techoral-security-trail" \
--start-time $(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \
--verbose
Sample Validation Output
Validating log files for trail arn:aws:cloudtrail:us-east-1:123456789012:trail/techoral-security-trail
2026-06-08T00:00:00Z to 2026-06-08T01:00:00Z
Digest file: s3://techoral-cloudtrail-logs-prod/AWSLogs/123456789012/CloudTrail-Digest/us-east-1/2026/06/08/digest-file.json.gz
Valid: 47 log files validated, 0 invalid, 0 missing
2026-06-08T01:00:00Z to 2026-06-08T02:00:00Z
Digest file: s3://techoral-cloudtrail-logs-prod/AWSLogs/123456789012/CloudTrail-Digest/us-east-1/2026/06/08/digest-file2.json.gz
Valid: 52 log files validated, 0 invalid, 0 missing
Results requested for 2026-06-07T14:05:08Z to 2026-06-08T14:05:08Z
Results found for 2026-06-07T14:05:08Z to 2026-06-08T14:05:08Z:
24/24 digest files valid
936/936 log files valid
1 invalid, it means the log file on S3 has been modified after CloudTrail delivered it. This is strong forensic evidence of log tampering — escalate immediately to your incident response team and preserve all evidence before any remediation.To protect integrity further: enable S3 Object Lock (WORM) on the CloudTrail bucket with a retention period matching your compliance requirement (PCI-DSS requires 1 year minimum, HIPAA requires 6 years). Use Governance mode during initial setup to allow authorised corrections, then transition to Compliance mode.
10. Compliance Use Cases — PCI-DSS, SOC2, HIPAA
CloudTrail is a core control cited in every major compliance framework for cloud environments. Here is exactly how it maps to each standard and what evidence it generates for auditors.
PCI-DSS v4.0
Payment Card Industry requirements directly addressed by CloudTrail:
- Req 10.2: Log all individual user access to cardholder data — enable S3 data events on buckets containing payment data
- Req 10.2.1: Log all actions taken by individuals with root or administrative privileges — multi-region trail with management events covers this
- Req 10.2.2: Log all access to audit trails — CloudTrail itself logs access to CloudTrail (meta-audit) via management events on
cloudtrail.amazonaws.com - Req 10.2.3: Log invalid logical access attempts — metric filter on
errorCode = AccessDeniedanderrorCode = UnauthorizedOperation - Req 10.3: Protect audit trail from destruction and modification — S3 Object Lock + log file validation + restrictive bucket policy
- Req 10.5: Retain audit trail for at least 12 months — set S3 lifecycle rule to retain CloudTrail logs for 365 days minimum (hot) + archive to Glacier for years 2–7
# Generate a compliance evidence report: all admin actions in the last 90 days
aws athena start-query-execution \
--query-string "
SELECT eventtime, useridentity.arn, eventname, awsregion, sourceipaddress
FROM cloudtrail_logs
WHERE year IN ('2026') AND month IN ('03','04','05','06')
AND (useridentity.type = 'Root'
OR requestparameters LIKE '%AdministratorAccess%'
OR eventname LIKE '%Policy%'
OR eventname LIKE '%Permission%')
ORDER BY eventtime DESC
" \
--query-execution-context Database=default \
--result-configuration OutputLocation=s3://techoral-audit-reports/pci-dss/admin-actions/
SOC 2 Type II
SOC 2 evaluates five Trust Service Criteria. CloudTrail addresses all of them:
- CC6.1 (Logical Access): CloudTrail logs every IAM change, role assumption, and console login — the auditor's primary evidence for who has access to what
- CC6.2 (Registration/Removal):
CreateUser,DeleteUser,CreateAccessKey,DeleteAccessKeyevents show the lifecycle of identities - CC6.3 (Access Removal):
DetachUserPolicy,DetachRolePolicyevents confirm timely access revocation - CC7.2 (Monitoring): CloudTrail Insights + CloudWatch metric filter alarms constitute your continuous monitoring programme
- CC7.3 (Incident Detection): EventBridge + Lambda responder demonstrates automated incident detection and response
- A1.1 (Availability): CloudTrail being multi-region and delivering to S3 with 99.999999999% durability satisfies availability of audit logs
HIPAA
The HIPAA Security Rule requires audit controls for systems containing Electronic Protected Health Information (ePHI):
- §164.312(b) — Audit Controls: CloudTrail satisfies this directly — it records and examines activity in systems that contain ePHI on AWS
- §164.312(c)(1) — Integrity: Log file validation with digest files provides tamper-evidence for audit records
- §164.308(a)(5)(ii)(C) — Log-in Monitoring: CloudWatch metric filter on
ConsoleLoginwith alarm implements the required login monitoring - §164.316(b)(2)(i) — Retention: HIPAA requires 6-year retention — use S3 lifecycle: 2 years Standard → 4 years Glacier Deep Archive
# S3 lifecycle rule for HIPAA 6-year retention
aws s3api put-bucket-lifecycle-configuration \
--bucket "techoral-cloudtrail-logs-prod" \
--lifecycle-configuration '{
"Rules": [{
"ID": "CloudTrailHIPAARetention",
"Status": "Enabled",
"Filter": {"Prefix": "AWSLogs/"},
"Transitions": [
{"Days": 90, "StorageClass": "INTELLIGENT_TIERING"},
{"Days": 730, "StorageClass": "GLACIER"}
],
"NoncurrentVersionTransitions": [
{"NoncurrentDays": 30, "StorageClass": "GLACIER"}
],
"Expiration": {"Days": 2190}
}]
}'
Frequently Asked Questions
How long does it take for CloudTrail events to appear in S3?
CloudTrail delivers log files to S3 within approximately 15 minutes of the API event occurring. For near-real-time analysis (<2 minutes), use the CloudWatch Logs integration or EventBridge. CloudTrail Lake typically makes events available for querying within 5 minutes.
Is CloudTrail enabled by default?
AWS enables a single-region CloudTrail event history (not a full trail) for every new account — it stores 90 days of management events visible in the console and is free. However, this is not a trail: events are not delivered to S3, not integrated with CloudWatch Logs, and not available for Athena queries. You must create an explicit trail to get durable, queryable logs.
Can I use CloudTrail to audit cross-account role assumptions?
Yes — AssumeRole events appear in the CloudTrail of the account that owns the role. The userIdentity.principalId includes both the calling account and the assumed role session name. For complete cross-account visibility, aggregate logs from all accounts into a single S3 bucket using an Organisation Trail — then Athena can query across all accounts in a single SQL statement.
What events does CloudTrail NOT log?
CloudTrail does not log: SSH/RDP sessions to EC2 instances (use AWS Systems Manager Session Manager for that audit trail), data flowing through VPCs (use VPC Flow Logs), DNS queries (use Route 53 Resolver query logging), or the content of S3 objects (only the API call metadata, not the object itself). Build a complete audit programme using CloudTrail + VPC Flow Logs + Route 53 query logs + Systems Manager Session Manager logs.
How do I reduce CloudTrail costs without losing security coverage?
Key optimisations: (1) Use ReadWriteType: WriteOnly for management events — read-only calls like Describe* and List* generate 70% of management event volume with minimal security value. (2) Use Advanced Event Selectors to scope data events to specific bucket ARNs or Lambda function ARNs rather than all resources. (3) Enable CloudTrail Lake instead of S3+Athena to avoid redundant storage and query costs. (4) Archive S3 logs to Glacier after 90 days — Athena can still query them with S3 Select Glacier Restore.