CloudTrail Investigation: How to Trace Suspicious Activity in AWS
Last verified on Nov 8, 2026.
CloudTrail records API calls and console sign-ins in your AWS account, and it is the first place to look when you ask who did something, from where, and what else they did. An investigation means choosing a pivot such as an access key, a user, or an IP address, pulling every event for it, then widening the search to the actions that attackers tend to take. This guide covers the record structure, where to run queries, and a set of ready queries for Logs Insights and Athena.
Read a CloudTrail Event
Most of an investigation uses the fields below from the event record.
userIdentity.type tells you the kind of principal. AWS documents types including Root, IAMUser, AssumedRole, and FederatedUser. For temporary credentials, the sessionIssuer element shows the role or user that issued the session, which is how you follow an ASIA key back to its source. awsRegion matters too: events are recorded in the Region where they happened.
Two cautions. The sourceIPAddress for actions originating in the console is the address of the underlying customer resource, not the console web server, and for calls from inside AWS it may be a DNS name or AWS Internal/<number>. And errorCode is only sometimes top-level: some services report error information inside responseElements.
Choose Where to Query
Event history is a searchable, downloadable, immutable record of 90 days of management events in a Region, with no charge to view it. lookup-events takes a time range and one lookup attribute: AccessKeyId, EventName, EventSource, and Username, among others.
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=Username,AttributeValue=alice \
--start-time 2026-11-01T00:00:00Z --end-time 2026-11-08T00:00:00Z \
--max-results 50 --output json \
| jq -r '.Events[].CloudTrailEvent | fromjson
| [.eventTime, .eventSource, .eventName, .sourceIPAddress, .userAgent, (.errorCode // "-")] | @tsv'Run it in every Region you use. Event history shows no data events such as S3 object reads, and lookup-events accepts only one lookup attribute per query, so filter on a second attribute, such as the source address, in jq. For a single leaked key, see leaked AWS access key. To catch privilege-granting calls such as CreateAccessKey as they happen, see the privilege changes detection guide. GuardDuty findings are a good source of pivots, since each names the principal, API, and source address to start from. To send trails into a log platform, the AWS CloudTrail integration covers the setup.
Investigation Queries
These queries are written for CloudWatch Logs Insights, over the log group a trail delivers to. Insights discovers JSON fields automatically, so nested fields such as userIdentity.arn can be addressed directly. Athena equivalents follow where the shape differs.
Failed console sign-ins. A failed IAM user sign-in is a ConsoleLogin event from signin.amazonaws.com with errorMessage of Failed authentication and responseElements.ConsoleLogin set to Failure.
filter eventName = "ConsoleLogin" and responseElements.ConsoleLogin = "Failure"
| stats count(*) as failures by userIdentity.userName, sourceIPAddress
| sort failures descMany failures from one address against many names is password spraying. Many failures against one name, then a success from the same address, is the pattern to escalate. Check the successes too: filter responseElements.ConsoleLogin = "Success" and compare additionalEventData.MFAUsed.
Persistence: new identities, keys, and policies. GuardDuty describes persistence APIs as typically create or modify operations, such as CreateAccessKey:
filter eventName in ["CreateUser", "CreateAccessKey", "CreateLoginProfile", "AttachUserPolicy", "PutUserPolicy", "AttachRolePolicy", "UpdateAssumeRolePolicy"]
| fields @timestamp, eventName, userIdentity.arn, sourceIPAddress, requestParameters.userName, errorCode
| sort @timestamp descDefense evasion: logging changes. GuardDuty describes defense evasion as delete, disable, or stop operations such as StopLogging:
filter eventName in ["StopLogging", "DeleteTrail", "UpdateTrail", "PutEventSelectors"]
| fields @timestamp, eventName, userIdentity.arn, sourceIPAddress, errorCode
| sort @timestamp descBucket exposure. Changes to bucket policies, ACLs, and public access blocks:
filter eventSource = "s3.amazonaws.com" and eventName in ["PutBucketPolicy", "PutBucketAcl", "DeleteBucketPolicy", "PutBucketPublicAccessBlock", "DeletePublicAccessBlock"]
| fields @timestamp, eventName, userIdentity.arn, sourceIPAddress, requestParameters.bucketNameSecret reads in bursts. One application reading its own secret on startup is normal. A new principal reading many secrets in minutes is not:
filter eventSource = "secretsmanager.amazonaws.com" and eventName = "GetSecretValue"
| stats count(*) as reads by bin(5m), userIdentity.arn, sourceIPAddress
| sort reads desc
| limit 20Root and failed calls. Root activity should be rare, and a run of different failing calls is often enumeration:
filter userIdentity.type = "Root"
| stats count(*) by eventName, sourceIPAddress
filter ispresent(errorCode)
| stats count(*) as errors by userIdentity.arn, errorCode
| sort errors descThe Same Questions in Athena
With the partition-projected table from AWS's documentation, cloudtrail_logs_pp, always filter on the timestamp partition (formatted yyyy/MM/dd) to limit the scan. In that table responseelements is a string, so extract the field:
SELECT eventtime, useridentity.username, sourceipaddress, useragent, errormessage
FROM cloudtrail_logs_pp
WHERE "timestamp" >= '2026/11/01'
AND eventname = 'ConsoleLogin'
AND json_extract_scalar(responseelements, '$.ConsoleLogin') = 'Failure'
ORDER BY eventtime DESC
LIMIT 100;SELECT useridentity.arn, sourceipaddress,
date_trunc('minute', from_iso8601_timestamp(eventtime)) AS minute,
count(*) AS reads
FROM cloudtrail_logs_pp
WHERE "timestamp" >= '2026/11/01'
AND eventsource = 'secretsmanager.amazonaws.com'
AND eventname = 'GetSecretValue'
GROUP BY 1, 2, 3
HAVING count(*) > 20
ORDER BY reads DESC;Judge What You Find
A single odd event proves little. Look for sequences: a failed login streak, then a success, then CreateAccessKey, then calls from a Region you do not use. Build a timeline by principal and by source address, mark the first event that should not have happened, and work backwards for how access began and forwards for what it reached.
Check the record itself. A stopped trail leaves a gap, which the logging-change query above would show. CloudTrail log file integrity validation delivers digest files so you can detect tampering with delivered logs, and it is enabled by default for new trails.
Searching CloudTrail Events in Sazabi
Sazabi, an AI-native observability and security platform, streams CloudTrail events from your trail's CloudWatch Logs group. Each event keeps CloudTrail's own JSON, so eventName, eventSource, userIdentity, sourceIPAddress, and errorCode remain searchable fields.
Frequently Asked Questions
How far back can I investigate in CloudTrail?
Event history covers the past 90 days of management events per Region. For anything older, or for data events, you need a trail that delivers to S3 or CloudWatch Logs, or a CloudTrail Lake event data store, created before the activity happened.
What is the difference between lookup-events and a trail?
lookup-events searches event history, which is on by default, limited to 90 days of management events and one lookup attribute per query. A trail delivers events to S3 and optionally CloudWatch Logs, which you can keep longer and query with Athena or Logs Insights.
Which CloudTrail events are most worth alerting on?
Root user activity, failed console sign-ins, creation of users and access keys, policy attachments, any change to logging such as StopLogging or DeleteTrail, bucket policy changes, and bursts of secret reads.
Can an attacker hide their activity from CloudTrail?
They can try to stop or delete a trail, which is why those events matter and why log file integrity validation and a separate log archive account help. Event history is immutable and keeps recording management events even if a trail is stopped.
Sources
- CloudTrail record fields: eventTime, userIdentity, eventSource, eventName, awsRegion, sourceIPAddress, userAgent, errorCode, and errorMessage (accessed Nov 8, 2026)
- The userIdentity element describes the identity that made the request, with type values such as Root, IAMUser, AssumedRole, and FederatedUser and a sessionIssuer element for temporary credentials (accessed Nov 8, 2026)
- Event history is a searchable, downloadable, immutable record of the past 90 days of management events in a Region, with no charge to view it (accessed Nov 8, 2026)
- lookup-events takes start and end times and one lookup attribute, such as AccessKeyId, EventName, EventSource, or Username (accessed Nov 8, 2026)
- A failed IAM user console sign-in is a ConsoleLogin event from signin.amazonaws.com with errorMessage Failed authentication and responseElements ConsoleLogin Failure (accessed Nov 8, 2026)
- CloudWatch Logs Insights sample queries for CloudTrail logs using filter, stats count(*) by, ispresent, and in [ ] with fields such as eventName and requestParameters (accessed Nov 8, 2026)
- Athena can query CloudTrail logs in S3 with a CREATE EXTERNAL TABLE using partition projection, with columns such as useridentity, eventname, sourceipaddress, and a timestamp partition (accessed Nov 8, 2026)
- CloudTrail log file integrity validation delivers digest files and is enabled by default for new trails (accessed Nov 8, 2026)
- GuardDuty describes defense evasion APIs as delete, disable, or stop operations such as StopLogging, and persistence APIs as create or modify operations such as CreateAccessKey (accessed Nov 8, 2026)
- Sazabi streams CloudTrail events from the trail CloudWatch Logs group and keeps eventName, eventSource, userIdentity, sourceIPAddress, and errorCode searchable (accessed Nov 8, 2026)