API activity tracking
CloudTrail answers the question every incident eventually asks: who did that, and when? This standard covers enabling it everywhere, centralising it, and turning it into alerts rather than an archive nobody reads.
The standard. End-to-end API and activity tracking through CloudTrail, with centralised retention, alerting and a queryable audit trail.
What CloudTrail records
Every API call made to an AWS account — from the console, the CLI, an SDK, or another AWS service — is recorded as an event.
| Event type | Contents | Default |
|---|---|---|
| Management events | Creation, modification and deletion of resources; permission changes | Recorded |
| Data events | Object-level S3 operations, Lambda invocations, DynamoDB item operations | Not recorded by default |
| Insights events | Anomalous API call rates and error rates | Not recorded by default |
Management events answer "who changed the infrastructure." Data events answer "who read this object." The second is usually the more sensitive question and costs more to store.
Configuration
Organisation trail
A single trail in the management account delivers from every member account and region to the log archive account.
resource "aws_cloudtrail" "organisation" {
name = "organisation-trail"
s3_bucket_name = aws_s3_bucket.log_archive.id
include_global_service_events = true
is_multi_region_trail = true
is_organization_trail = true
enable_log_file_validation = true
kms_key_id = aws_kms_key.trail.arn
event_selector {
read_write_type = "All"
include_management_events = true
data_resource {
type = "AWS::S3::Object"
values = ["arn:aws:s3:::sensitive-bucket/"]
}
}
insight_selector {
insight_type = "ApiCallRateInsight"
}
insight_selector {
insight_type = "ApiErrorRateInsight"
}
}
| Setting | Value | Reason |
|---|---|---|
| Multi-region | Yes | A trail scoped to one region leaves blind spots |
| Organisation trail | Yes | New accounts are covered without per-account setup |
| Log file validation | Yes | Proves logs were not altered after delivery |
| KMS encryption | Yes | The log contains sensitive metadata |
| Retention | 1 year hot, 10 years for access logs | Matches audit and contractual requirements |
Centralised delivery
Member account ──► CloudTrail ──► Log archive account / S3
│
├─ KMS encrypted
├─ Object Lock (optional, compliance)
└─ Lifecycle → Glacier
The log archive account is write-only from member accounts. Even an administrator in a workload account cannot alter the audit trail.
From logs to alerts
Metric filters
aws logs put-metric-filter \
--log-group-name "/aws/cloudtrail/organisation" \
--filter-name "UnauthorizedAPICalls" \
--filter-pattern '{ ($.errorCode = "UnauthorizedOperation") || ($.errorCode = "AccessDenied") }' \
--metric-transformations \
metricName=UnauthorizedAPICalls,metricNamespace=SecurityMetrics,metricValue=1
Alarms worth having
| Alarm | Filter | Why |
|---|---|---|
| Root account usage | userIdentity.type = "Root" | Root should not be used operationally |
| Unauthorised API calls | errorCode = AccessDenied or UnauthorizedOperation | Misconfiguration or probing |
| IAM policy change | eventName = PutRolePolicy / AttachRolePolicy | Privilege escalation path |
| CloudTrail disabled | eventName = StopLogging / DeleteTrail | Attacker covering tracks |
| Security group opened | eventName = AuthorizeSecurityGroupIngress | Exposure of a service |
| S3 bucket policy change | eventName = PutBucketPolicy | Data exposure |
| KMS key scheduled for deletion | eventName = ScheduleKeyDeletion | Data becomes unrecoverable |
| Console login without MFA | eventName = ConsoleLogin with MFA false | Credential compromise |
| GuardDuty finding | EventBridge rule | Threat detection response |
EventBridge routing
CloudTrail events reach EventBridge, where rules route them to an SNS topic, a ticketing system, or an automated response.
{
"source": ["aws.iam"],
"detail-type": ["AWS API Call via CloudTrail"],
"detail": {
"eventName": ["CreateAccessKey", "AttachUserPolicy", "CreateUser"]
}
}
Analysis
Common investigations
| Question | Query approach |
|---|---|
| Who deleted this resource? | eventName = Delete*, filter by resource ARN |
| When did this permission change? | eventName in IAM mutation set, sorted by time |
| Which principal accessed this bucket? | Data events for the bucket prefix |
| Did anyone use the root account? | userIdentity.type = Root |
| Is this IP normal for this user? | Compare source IP across the identity's history |
Logs Insights
fields @timestamp, eventSource, eventName, userIdentity.arn, sourceIPAddress, errorCode
| filter eventName like /Delete|Detach|Put/
| filter @timestamp > ago(24h)
| sort @timestamp desc
| limit 100
Retention and access
| Data | Retention | Access |
|---|---|---|
| CloudTrail management events (hot) | 12 months | Security team; read-only for auditors |
| CloudTrail archive | 10 years | Security team; retrieval workflow |
| Data events | Per classification | Restricted to the owning team and security |
| Access logs to the log bucket | 10 years | Security team |
| Config history | Per compliance requirement | Security and compliance |
Evidence
- Trail configuration export showing multi-region and organisation scope.
- Log archive bucket policy preventing member-account modification.
- Alarm inventory with the metric filter behind each.
- A sample investigation showing a change traced to a principal.
- Retention configuration.