secskills
secskills / defense / investigating-aws-incidents

investigating-aws-incidents

defense verified 2026-07-26

Investigate security incidents in Amazon Web Services -- reconstruct attacker activity from CloudTrail, VPC Flow Logs, and GuardDuty, anchor the investigation on the compromised principal (access key or role), trace privilege escalation and persistence through IAM API calls, detect data exfiltration and crypto-mining, and contain without destroying evidence or tipping off the attacker. Use when responding to a suspected AWS compromise, exposed access keys, anomalous CloudTrail activity, a GuardDuty finding, unexpected IAM changes, crypto-mining EC2 instances, or S3 data exfiltration.

$ /plugin install secskills-defense $ /plugin install secskills-core

In AWS an incident is reconstructed from logs the attacker usually could not delete -- CloudTrail, VPC Flow Logs, and the control plane's own record of every API call. The investigation is therefore a log-correlation exercise anchored on the compromised principal: the access key or assumed role, and the sequence of API calls it made. Find the principal, pull every event it generated, and the rest of the intrusion falls out of the timeline.

When to Use

When NOT to Use

First-Hour Triage

Three actions, in order: identify the principal, pull its recent activity, preserve before you contain.

Scope the compromised principal. Whether the report is a leaked key, a GuardDuty finding, or a billing spike, resolve it to a single principal ARN -- an IAM user, a role, or the root account. That ARN is the anchor for everything that follows.

# What is this key/session, and does it still work?
aws sts get-caller-identity                       # if you hold the suspect creds
# Resolve an access key ID to its owner
aws iam get-access-key-last-used --access-key-id AKIA...
# Enumerate the principal's current footprint
aws iam list-access-keys --user-name <user>
aws iam list-attached-user-policies --user-name <user>
aws iam list-user-policies --user-name <user>

Pull recent activity from the principal. Event history (below) is the fastest first look; the S3 log bucket is the source of truth.

aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=Username,AttributeValue=<user> \
  --start-time 2026-07-01T00:00:00Z --max-results 200

# By access key -- catches role sessions that lookup-by-username misses
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=AccessKeyId,AttributeValue=AKIA...

Isolate without tipping off -- snapshot then contain. An attacker who sees their key deactivated mid-operation will burn persistence you have not found yet. For anything but active, ongoing damage: preserve evidence first (snapshot volumes, export logs), map persistence, then contain everything at once. If there is live damage -- active mining, active exfil -- stop the damage and accept the trade.

CloudTrail Deep-Dive

Event history vs. the S3 log bucket. The console Event history is searchable but covers only 90 days of management events and drops data events. The CloudTrail S3 log bucket (or CloudTrail Lake) is the authoritative record and the only place with S3/Lambda data events -- query it, not the console, once you are past the first look. Confirm what is actually logged:

aws cloudtrail describe-trails
aws cloudtrail get-trail-status --name <trail>          # IsLogging: true?
aws cloudtrail get-event-selectors --trail-name <trail> # data events on?

userIdentity types tell you what you are looking at:

typeMeaningInvestigative note
IAMUserLong-lived user credentialsaccessKeyId is the pivot. Human or service account?
AssumedRoleTemporary STS credsRead sessionContext.sessionIssuer for the source role.
AWSServiceAn AWS service actingUsually benign, but check for spoofed-looking service calls.
RootRoot accountAlmost never legitimate for API calls. Treat as critical.
FederatedUser / WebIdentityUserGetFederationToken broker / OIDC web identityTrace to the broker's IAM user, or the IdP session.

For AssumedRole, sessionContext.sessionIssuer.arn names the role and sessionContext.attributes.mfaAuthenticated tells you whether MFA was used.

Anomaly fields to grep:

inside the VPC (the IMDS theft signature, below). Correlate against VPC Flow Logs.

kali on a principal that normally shows console or SDK-from-Lambda agents.

mining in ap-* / sa-* to dodge attention). Enumeration touches many regions fast.

enumeration: the attacker is mapping what the stolen principal can do.

-- Athena over the CloudTrail S3 bucket: AccessDenied enumeration storm
SELECT eventname, count(*) AS n
FROM cloudtrail_logs
WHERE useridentity.accesskeyid = 'AKIA...'
  AND errorcode = 'AccessDenied'
  AND eventtime > '2026-07-01T00:00:00Z'
GROUP BY eventname ORDER BY n DESC;

Canonical Attacker API Patterns

Grep the timeline for this sequence. It is the shape of nearly every stolen-key intrusion:

confirming what the key is and who owns the account.

Secrets Manager, often with an AccessDenied storm.

AttachUserPolicy (watch for AdministratorAccess), PutUserPolicy (inline policy so it does not show in attached-policy lists), CreateLoginProfile (a console password on an API-only account), UpdateLoginProfile.

instance, Lambda, or Glue job to inherit its permissions. Look for PassRole paired with RunInstances / CreateFunction.

trust policy naming an external account (a cross-account backdoor).

building a chain that launders the original stolen key.

# Pull the IAM mutating calls for the window
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=CreateAccessKey
# Repeat for: CreateUser, AttachUserPolicy, PutUserPolicy, CreateLoginProfile,
#             UpdateAssumeRolePolicy, CreateRole, AssumeRole
# (iam:PassRole is a permission, not an event -- find it in requestParameters of
#  RunInstances / CreateFunction, not via lookup-by-EventName)

Persistence Hunting

Enumerate every mechanism explicitly during eradication -- rotating the one leaked key does nothing about a second one the attacker created.

victim or another account; a user may have up to two keys.

principal that never used the console is high-signal.

external principal or sts:AssumeRole from another account.

re-creates users, often on an EventBridge schedule.

# Users and keys created recently
aws iam list-users --query 'Users[?CreateDate>=`2026-07-01`].[UserName,CreateDate]'
for u in $(aws iam list-users --query 'Users[].UserName' --output text); do
  aws iam list-access-keys --user-name "$u" \
    --query 'AccessKeyMetadata[].[UserName,AccessKeyId,CreateDate]' --output text
done

# Roles whose trust policy names an external account
aws iam list-roles --query 'Roles[].[RoleName,AssumeRolePolicyDocument]'

# Recently modified Lambda and the EventBridge rules that fire them
aws lambda list-functions --query 'Functions[].[FunctionName,LastModified]'
aws events list-rules --query 'Rules[].[Name,ScheduleExpression,State]'

IMDS / SSRF Credential Theft Signatures

The classic AWS escalation: an SSRF or foothold on an EC2 instance reads the Instance Metadata Service (http://169.254.169.254/latest/meta-data/iam/...), lifts the instance role's temporary credentials, and uses them elsewhere.

The signature is unmistakable in CloudTrail: the instance role's session credentials appear from a sourceIPAddress that is not the instance. The role is minted for the instance, so any call from an external or unrelated IP means the credentials left the box.

-- Role session creds used from outside the VPC
SELECT eventtime, eventname, sourceipaddress, useragent
FROM cloudtrail_logs
WHERE useridentity.arn LIKE '%assumed-role/<instance-role>%'
  AND sourceipaddress NOT LIKE '10.%'
  AND sourceipaddress NOT LIKE '172.%'
ORDER BY eventtime;

IMDSv1 (a simple GET, no session token) makes this trivial and is itself a finding -- confirm whether the instance enforces IMDSv2 (HttpTokens: required) via aws ec2 describe-instances. Correlate the theft window with VPC Flow Logs for the outbound SSRF and the reuse source.

GuardDuty Finding Triage

GuardDuty is a starting pistol, not the investigation. Each finding type maps to a hypothesis you confirm in CloudTrail:

Finding typeImplication
UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.*Instance role creds used off the instance -- the IMDS theft above.
UnauthorizedAccess:IAMUser/MaliciousIPCallerA principal called from a known-bad IP.
UnauthorizedAccess:IAMUser/TorIPCallerAPI calls via Tor -- almost never a legitimate admin.
CryptoCurrency:EC2/BitcoinTool.B!DNSAn instance talking to a mining pool -- confirmed crypto-mining.
Recon:IAMUser/*Enumeration -- the AccessDenied storm.
Persistence:IAMUser/*, PrivilegeEscalation:IAMUser/*IAM mutation matching the escalation patterns above.
Policy:S3/BucketAnonymousAccessGrantedA bucket was made public -- possible exfil staging.
aws guardduty list-findings --detector-id <id> \
  --finding-criteria '{"Criterion":{"severity":{"Gte":4}}}'
aws guardduty get-findings --detector-id <id> --finding-ids <id>

Findings older than the CloudTrail window still carry the principal and IPs -- pivot on those even when the raw events have aged out of Event history.

Data-Theft Detection

buckets, visible only if S3 data events are logged. Check request counts by principal.

AllUsers or AuthenticatedUsers, or PutPublicAccessBlock disabling the block.

adding an external account or all to the volume/AMI (steal data by sharing the snapshot out).

StartExportTask dumping a snapshot to an attacker-controlled S3 bucket.

SELECT eventname, useridentity.arn, sourceipaddress,
       json_extract_scalar(requestparameters, '$.attributeType') AS attr
FROM cloudtrail_logs
WHERE eventname IN ('ModifySnapshotAttribute','ModifyImageAttribute',
                    'ModifyDBSnapshotAttribute','PutBucketPolicy','PutBucketAcl')
  AND eventtime > '2026-07-01T00:00:00Z';

Anti-Forensics the Attacker Attempts

A capable attacker tries to blind you. Watch for and, crucially, work around:

events or a whole region) without deleting the trail.

GuardDuty.

The defeat for all of these is a member-account-independent Organizations-level trail that logs to a central, locked S3 bucket (MFA delete, Object Lock, separate log-archive account) the compromised principal cannot reach. The very act of StopLogging is itself logged there before it takes effect. If you have an org trail, the attacker's cleanup calls are evidence, not gaps. If you do not, note the blind window as a scoping limitation.

# Was logging tampered with? These calls are the finding.
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=StopLogging
# Repeat: DeleteTrail, PutEventSelectors, DeleteFlowLogs, DeleteDetector

Evidence Preservation

instance. Tag with the case ID and apply a resource policy / legal hold so they cannot be deleted.

``bash aws ec2 create-snapshot --volume-id vol-xxx \ --description "IR-2026-042 preserve" \ --tag-specifications 'ResourceType=snapshot,Tags=[{Key=case,Value=IR-2026-042},{Key=legal-hold,Value=true}]' ``

implant or in-memory creds, this is the only source. Acquire and then analyze per analyzing-memory-images.

VPC Flow Logs and any GuardDuty findings before retention expires.

collection if the incident may become a regulatory or litigation matter.

Containment and Eradication

Do it all at once, after scoping. Partial containment alerts the attacker.

deactivated key still shows in the timeline.

``bash aws iam update-access-key --user-name &lt;user&gt; \ --access-key-id AKIA... --status Inactive ``

temporary sessions minted from it. Attach an inline deny policy that invalidates any session/token issued before now (the AWSRevokeOlderSessions pattern):

``bash aws iam put-user-policy --user-name &lt;user&gt; \ --policy-name AWSRevokeOlderSessions \ --policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Deny", "Action":"*","Resource":"*","Condition":{"DateLessThan": {"aws:TokenIssueTime":"2026-07-20T00:00:00Z"}}}]}' ``

For a role, put an equivalent deny in the role's inline policy or a boundary.

Secrets Manager / SSM the principal read, and any hardcoded creds on compromised instances.

no egress rather than terminating it, so memory and disk survive for analysis.

tampered trust policies, remove backdoor Lambda and EventBridge rules -- only after they are documented.

Rationalizations to Reject

behaviors and depends on being enabled in the right regions. Absence of a finding is not evidence of absence -- the CloudTrail timeline is authoritative.

second key, a new user, a modified trust policy, or a backdoor Lambda survives rotation. Enumerate every persistence mechanism before you close.

their tooling on EC2 too. Correlate the principal, user agent, and behavior -- not just the IP owner.

(PutUserPolicy), a second access key on an existing user, and modified role trust policies all grant persistence without ever creating a user.

RunInstances was compromised -- the same access could exfiltrate data or escalate. Mining is the visible symptom, not the scope.

iam:PassRole and AssumeRole chains routinely turn a modest instance role into account-wide access. Trace what the role can reach, don't assume.

is your visibility limit, not the attacker's timeline. Record it as a scoping gap and check the org trail / Lake for longer retention.

References