Detecting Privilege Escalation in AWS: A CloudTrail Runbook

Detecting privilege escalation in AWS comes down to watching a short list of CloudTrail events that healthy workloads almost never call. An attacker with a foothold in your account does not need a zero-day to become an administrator; they need one overly permissive IAM permission and a single API call to exercise it. Every one of those calls — creating a policy version, attaching a policy, minting an access key — lands in CloudTrail within minutes. Rhino Security Labs catalogued 21 distinct ways for a low-privilege principal to reach administrative permissions, and nearly every one of those paths produces a short, loud event trail in CloudTrail. Teams that catch escalation early alert on those events by name instead of waiting for anomaly models to flag them hours later.
Why Escalation Beats Exploits
Most cloud intrusions are configuration abuse, not vulnerability exploitation. The IAM service is working as designed when it lets an authorized caller modify policies; the abuse lives in how broad that authorization was. The canonical example from the Rhino research: a new policy version created with the –set-as-default flag becomes the default without ever requiring iam:SetDefaultPolicyVersion, so a single CreatePolicyVersion permission is enough to rewrite effective permissions in one call. The same pattern repeats across update and attach operations: SetDefaultPolicyVersion, AttachUserPolicy, PutRolePolicy and UpdateAssumeRolePolicyDocument each hand a caller the ability to widen privileges somewhere else in the account. Because these are normal administrative APIs, the defense cannot block them outright — it has to detect the context in which they fire.
The Events Worth Alerting On
The table below maps the highest-signal CloudTrail events to the escalation capability each one unlocks and the triage question that separates abuse from routine change management. Deploy pipelines legitimately call some of these actions, so compare each occurrence against your change window and the calling identity before paging.
| CloudTrail event | What it enables | Triage question |
|---|---|---|
| CreatePolicyVersion | Full control of effective permissions via –set-as-default | Did the caller come from a known deployment role? |
| SetDefaultPolicyVersion | Reactivates an older, broader policy version | Was this version part of the last reviewed change? |
| AttachUserPolicy / PutUserPolicy | Grants new permissions to any chosen principal | Why is a human attaching policies outside IaC? |
| CreateAccessKey | Persistent credentials for another user | Does the target user already hold two active keys? |
| UpdateAssumeRolePolicyDocument | Edits who may assume a role | Was the trust change requested through IaC? |
| PassRole with RunInstances or CreateFunction | Moves a powerful role onto attacker-controlled compute | Is the destination resource in the known fleet? |
Two detection details decide whether these alerts are usable. First, alert on the pairing, not the single event: PassRole alone is routine, but PassRole followed by RunInstances from an unfamiliar subnet is a classic takeover pattern documented in the Rhino Security Labs escalation research and its companion method catalog on GitHub. Second, baseline by identity: an event that is normal for your CI role is an emergency when it comes from a contractor account.
A Five-Step Response Runbook
When one of these alerts fires, work the sequence in order and resist the urge to investigate before containing.
- Freeze the caller. Attach an explicit deny-all inline policy to the suspected identity and revoke its active sessions; this stops in-flight escalation without destroying evidence.
- Establish the timeline. Pull every CloudTrail event from that caller in the preceding 24 hours, including GetCallerIdentity and any AssumeRole calls, to map what was read before anything was changed.
- Roll back the specific change. Delete rogue policy versions, detach unauthorized policies, deactivate any access keys created during the window, and revert modified trust policies.
- Hunt lateral movement. Search for AssumeRole calls made by the compromised identity toward roles it had never assumed before, and check for new Lambda functions, EC2 instances or notebooks created with passed roles.
- Close the enabling gap. Remove the permission pair that made escalation possible and add the scoped Resource element your policy was missing, so the same path cannot be retried with the next foothold.
Cutting the Blast Radius
Detection gets faster when the architecture limits how far a stolen session can travel. Role chaining caps any AWS CLI or API session at a maximum of one hour, even when the role itself allows a longer maximum session duration, according to the AWS STS AssumeRole reference and the IAM role chaining documentation; that cap is a real containment control, because a pivoted credential dies on its own within the hour. On the preventive side, condition keys such as aws:SourceIp and aws:username let admins restrict dangerous permissions to a known address range or to the caller’s own identity, which breaks most of the escalation paths in the catalog at the policy layer. Reviewing those scoping decisions on a schedule is more effective than any single alert, and if your account still leans on static keys, the approach described in retiring static cloud keys with workload identity federation removes an entire credential-theft class. Finally, your mean time to answer depends on what you logged in the first place — the decisions covered in a cloud logging strategy built for fast investigations determine whether step two of the runbook takes minutes or a day.