AssumeRole in CloudTrail: Following Role Sessions Across Accounts
Published
A request to AWS STS for temporary credentials for an IAM role; the record names the caller, the role and the temporary access key it returned, which ties the role session to every call made with it.
In AWS's example records: eventType: AwsApiCalleventCategory: ManagementmanagementEvent: truereadOnly: true
AWS's description
Returns a set of temporary security credentials that you can use to access AWS resources. These temporary credentials consist of an access key ID, a secret access key, and a security token. Typically, you use AssumeRole within your account or for cross-account access. For a comparison of AssumeRole with other API operations that produce temporary credentials, see Requesting Temporary Security Credentials and Compare AWS STS credentials in the IAM User Guide.
Source: AWS API reference: AssumeRole · copied from AWS's documentation and refreshed weekly
Request parameters
The parameters AWS's API reference documents for AssumeRole. A key is shown only where AWS's example records show it.
| API parameter | Required | In the CloudTrail record |
|---|---|---|
| DurationSeconds | No | requestParameters.durationSeconds |
| ExternalId | No | not shown in AWS's examples |
| MinimumSessionTokenSize | No | not shown in AWS's examples |
| Policy | No | not shown in AWS's examples |
| PolicyArns.member.N | No | not shown in AWS's examples |
| ProvidedContexts.member.N | No | not shown in AWS's examples |
| RoleArn | Yes | requestParameters.roleArn |
| RoleSessionName | Yes | requestParameters.roleSessionName |
| SerialNumber | No | requestParameters.serialNumber |
| SourceIdentity | No | requestParameters.sourceIdentity |
| Tags.member.N | No | not shown in AWS's examples |
| TokenCode | No | not shown in AWS's examples |
| TransitiveTagKeys.member.N | No | not shown in AWS's examples |
Fields that matter in an investigation
| Field | What it tells you |
|---|---|
| 1requestParameters.roleArn | The role that was requested. Its account ID shows at a glance whether the call crossed into another account. |
| 2requestParameters.roleSessionName | A name the caller chooses freely. It becomes the last part of the session's ARN, so it labels every later call but proves nothing about who made them. |
| 3responseElements.credentials.accessKeyId | The temporary access key ID (ASIA…) that was issued. Every call signed with these credentials carries it in userIdentity.accessKeyId, which is how you follow the session. |
| 4responseElements.assumedRoleUser.arn | The identity the session acts as, arn:aws:sts::<account>:assumed-role/<role>/<session name>, as it appears in userIdentity.arn of later calls. |
| 5userIdentity.type | Who asked. IAMUser for a user's own keys, AssumedRole when a role session assumes another role (role chaining), AWSAccount in the role owner's copy of a cross-account call. |
| 6userIdentity.arn | The caller's identity. For role chaining this is itself an assumed-role ARN; check userIdentity.sessionContext.sessionIssuer.arn for the role behind it. |
| 7requestParameters.sourceIdentity | An identity the caller set for the session. In AWS's examples it is carried into the response and into the next chained session, unlike the session name. |
| 8sharedEventID | The same value on both accounts' copies of a cross-account AssumeRole, so the two records can be joined. |
| 9additionalEventData.RequestDetails.endpointType | global or regional. Requests to the global endpoint sts.amazonaws.com are logged in us-east-1 whatever Region served them. |
| 10errorCode | AccessDenied when the caller may not assume the role. In the recorded denied attempt the role ARN appears only in errorMessage, with requestParameters null. |
Every field is checked on each build against AWS's CloudTrail record contents and userIdentity documentation and the example and recorded AssumeRole records shown below.
What the record looks like
AWS's example: Example cross-account AWS STS API events in CloudTrail log files (source)
Numbered lines are the fields in the table.
AWS's example: Example cross-account AWS STS API events in CloudTrail log files
Numbered lines are the fields in the table.
AWS's example: Example AWS STS role chaining API event in CloudTrail log file
Source · public IP addresses replaced with documentation addresses
Numbered lines are the fields in the table.
AWS's example: Example AWS STS API event using the global endpoint in CloudTrail log file
Numbered lines are the fields in the table.
Recorded during an attack simulation: Stratus Red Team technique aws.credential-access.ec2-get-password-data
Show the recorded AssumeRole record
Numbered lines are the fields in the table.
A real CloudTrail record from a test detonation, published by Stratus Red Team (Datadog, Apache License 2.0) and anonymized there; account IDs, addresses, access key IDs and session tokens are replaced here with AWS's documentation values, and the operator's user name with "operator". The anonymizer also changes values such as the Region.
Recorded during an attack simulation: Stratus Red Team technique aws.credential-access.ec2-get-password-data
Show the recorded AssumeRole record
Numbered lines are the fields in the table.
A real CloudTrail record from a test detonation, published by Stratus Red Team (Datadog, Apache License 2.0) and anonymized there; account IDs, addresses, access key IDs and session tokens are replaced here with AWS's documentation values, and the operator's user name with "operator". The anonymizer also changes values such as the Region.
What it records
AssumeRole asks AWS STS for temporary credentials for an IAM role: an access key ID, a secret access key and a session token. The record shows who asked (userIdentity), which role (requestParameters.roleArn), the session name and optional settings such as durationSeconds, an MFA serialNumber, sourceIdentity and session tags, and what came back: responseElements.assumedRoleUser and responseElements.credentials with the temporary key ID, its expiration and the session token. The secret access key is not in the record, in AWS’s examples or in the recorded attack-simulation event.
AWS’s example records mark AssumeRole as a management event with readOnly true. A role assumed across accounts is recorded in both accounts and the copies share a sharedEventID; in the role owner’s copy, AWS’s example shows the caller only as AWSAccount and an account ID. Requests to the global endpoint sts.amazonaws.com are logged in us-east-1, with additionalEventData.RequestDetails giving endpointType and awsServingRegion.
When it matters
Many AWS intrusions pass through AssumeRole. A stolen access key is used to step into a more privileged role or into another account, and from then on the attacker appears as the role session, not as the user who was compromised (MITRE ATT&CK T1078.004). The records worth stopping on:
- A role in an account you do not recognise, or a caller account you do not recognise in the role owner’s copy.
- Role chaining (
userIdentity.typeAssumedRole) from a session that does not normally hop between roles, especially towards administrative roles. - Runs of
AccessDenied. One principal failing to assume many roles is enumeration: tools try role names until one trusts them. The recorded denied attempt names the role only inerrorMessage. - A trust policy change first. An
UpdateAssumeRolePolicyon the role shortly before the first assumption from a new account is a backdoor being used.
How to investigate
- Establish the caller:
userIdentity.arnandtype,sourceIPAddressanduserAgent. For a chained session, followuserIdentity.sessionContext.sessionIssuer.arnback to the role it came from and repeat. - Take
responseElements.credentials.accessKeyIdand list every later event whoseuserIdentity.accessKeyIdmatches. That is what the session did. - For a cross-account call, find the other account’s copy by
sharedEventID. - Check the role’s history for
UpdateAssumeRolePolicyand permission changes, and confirm that the session name and duration fit how the role is normally used.
Role sessions from the last day in Microsoft Sentinel, joined to what each session did:
let Sessions = AWSCloudTrail
| where TimeGenerated > ago(1d)
| where EventName == "AssumeRole" and isempty(ErrorCode)
| extend Request = parse_json(RequestParameters), Response = parse_json(ResponseElements)
| project AssumedAt = TimeGenerated, Caller = UserIdentityArn, CallerType = UserIdentityType,
RoleArn = tostring(Request.roleArn), SessionName = tostring(Request.roleSessionName),
TempKey = tostring(Response.credentials.accessKeyId), SourceIpAddress;
Sessions
| join kind=leftouter (
AWSCloudTrail
| where TimeGenerated > ago(1d)
| summarize Calls = count(), Actions = make_set(EventName, 30) by UserIdentityAccessKeyId
) on $left.TempKey == $right.UserIdentityAccessKeyId
| project AssumedAt, Caller, CallerType, RoleArn, SessionName, SourceIpAddress, Calls, Actions
Denied attempts, which name the role in the error message:
AWSCloudTrail
| where TimeGenerated > ago(1d)
| where EventName == "AssumeRole" and ErrorCode == "AccessDenied"
| extend Role = extract(@"on resource: (\S+)", 1, ErrorMessage)
| summarize Attempts = count(), Roles = make_set(Role, 50) by UserIdentityArn, SourceIpAddress
| where Attempts >= 5
In Athena, over a table on the trail’s S3 bucket (named cloudtrail_logs here):
SELECT eventtime, useridentity.arn AS caller,
json_extract_scalar(requestparameters, '$.roleArn') AS role_arn,
json_extract_scalar(requestparameters, '$.roleSessionName') AS session_name,
json_extract_scalar(responseelements, '$.credentials.accessKeyId') AS temp_key,
sourceipaddress
FROM cloudtrail_logs
WHERE eventname = 'AssumeRole' AND errorcode IS NULL
ORDER BY eventtime DESC
LIMIT 200; Related events
- AssumeRoleWithSAML AWS STS
- AssumeRoleWithWebIdentity AWS STS
- GetCallerIdentity AWS STS
- GetSessionToken AWS STS
- UpdateAssumeRolePolicy IAM
- GetFederationToken AWS STS
Detect it
MITRE ATT&CK: T1078.004
Frequently Asked Questions
- How do I find everything done with the credentials an AssumeRole returned?
- Take the temporary key ID from responseElements.credentials.accessKeyId; it starts with ASIA. Every later call signed with those credentials carries the same value in userIdentity.accessKeyId, and its userIdentity.arn matches responseElements.assumedRoleUser.arn. Searching forward from the AssumeRole time on either value lists the whole session.
- Why does the same AssumeRole appear twice?
- A role assumed across accounts is recorded in both accounts. AWS's example shows the caller's account seeing the IAM user who asked, and the role owner's account seeing only an AWSAccount identity with the caller's account ID. Both copies carry the same sharedEventID, which is how to join them.
- Why is AssumeRole marked readOnly true?
- AWS's example records give AssumeRole a readOnly value of true. A collection rule or detection that keeps only write events, a common way to cut CloudTrail volume, therefore drops every role assumption; keep AssumeRole explicitly, because it links one identity to the next.
- In which Region is AssumeRole logged?
- Requests made to the AWS STS global endpoint, sts.amazonaws.com, are logged in US East (N. Virginia), us-east-1, with endpointType global and the Region that served the request in awsServingRegion. Requests to a Regional STS endpoint are logged in that endpoint's Region.
Find every AssumeRole in your own CloudTrail logs
Load CloudTrail logs (the Records files CloudTrail writes to S3, or aws cloudtrail lookup-events output) into the free browser analyzer: logging changes, new access keys, administrator grants, root activity and failed console sign-ins are flagged, with event names linked to their pages here. Nothing is uploaded.