AWS IAM Role Created with External Trust


Description

Detects a successful AWS IAM CreateRole event whose trust policy allows sts:AssumeRole by a principal in another AWS account or by a wildcard principal. This configuration may provide persistent access if the principal can assume the role. The event alone does not establish that the role was assumed or that its creation was unauthorized.

Query · spl

`cloudtrail`
eventSource="iam.amazonaws.com"
eventName="CreateRole"

| where isnull(errorCode) OR errorCode="" OR lower(errorCode)="success"

| eval policy_raw='requestParameters.assumeRolePolicyDocument',
       role_name='requestParameters.roleName',
       role_arn='responseElements.role.arn',
       user_arn=coalesce(user_arn, 'userIdentity.arn'),
       src=coalesce(src, sourceIPAddress)

| where isnotnull(policy_raw)

| eval policy=if(match(policy_raw, "(?i)^%7b"), urldecode(policy_raw), policy_raw)

| rex field=role_arn "^arn:[^:]+:iam::(?<account_from_role_arn>[0-9]{12}):role/"

| eval role_account_id=coalesce(account_from_role_arn, recipientAccountId)
| eval statements_json=json_extract(policy, "Statement")
| eval statements=if(match(statements_json, "^\s*\["), json_array_to_mv(statements_json), statements_json)

| mvexpand statements

| eval effect=json_extract(statements, "Effect"),
       actions_json=json_extract(statements, "Action"),
       principal_json=json_extract(statements, "Principal")

| eval actions=if(match(actions_json, "^\s*\["), json_array_to_mv(actions_json), actions_json)

| mvexpand actions
| mvexpand actions

| eval trust_action=actions
| eval action_pattern=replace(replace(lower(trust_action), "[*]", "%"), "[?]", "_")

| where effect="Allow" AND like("sts:assumerole", action_pattern)
| eval aws_json=json_extract(principal_json, "AWS")

| eval principals=case(
    match(aws_json, "^\s*\["), json_array_to_mv(aws_json),
    isnotnull(aws_json), aws_json,
    principal_json="*", "*"
  )

| mvexpand principals

| eval trust_principal=principals

| rex field=trust_principal "^arn:[^:]+:iam::(?<principal_account_from_arn>[0-9]{12}):"

| eval principal_account_id=if(
    match(trust_principal, "^[0-9]{12}$"),
    trust_principal,
    principal_account_from_arn
  )

| eval trust_type=case(
    trust_principal="*", "wildcard",
    isnotnull(role_account_id) AND isnotnull(principal_account_id)
      AND principal_account_id!=role_account_id, "external"
  )

| where trust_type="wildcard" OR trust_type="external"

| eval event_key=coalesce(eventID, md5(_raw))

| dedup event_key trust_principal

| table _time eventID user_arn src role_name role_arn role_account_id
        trust_type trust_principal principal_account_id trust_action

| `aws_iam_role_created_with_external_trust_filter`

Implementation guide

You must install Splunk AWS Add on and enable CloudTrail logs in your AWS Environment.

Known false positives

  • Approved cross-account integrations and deliberately configured wildcard trust may match. Review the role owner, trust-policy conditions, role permissions, and whether the principal is expected.

Analyst notes

Known false positives: Approved cross-account integrations and deliberately configured wildcard trust may match. Review the role owner, trust-policy conditions, role permissions, and whether the principal is expected.

Raw source AWS IAM Role Created with External Trust · SPL
Esc
Published by splunk/security_content ↗, licensed under Apache 2.0 ↗. Reproduced here unmodified.
name: AWS IAM Role Created with External Trust
id: 6b7f646b-5ddc-48c9-baa3-fd66ac202c18
version: 1
creation_date: '2026-10-13'
modification_date: '2026-10-13'
author: Maria Jose Erquiaga, Splunk
status: production
type: TTP
description: |-
    Detects a successful AWS IAM CreateRole event whose trust policy allows sts:AssumeRole by a principal in another AWS account or by a wildcard
    principal.
    This configuration may provide persistent access if the principal can assume the role. The event alone does not establish that the role was assumed or that its creation was unauthorized.
data_source:
    - AWS CloudTrail
search: |-
    `cloudtrail`
    eventSource="iam.amazonaws.com"
    eventName="CreateRole"

    | where isnull(errorCode) OR errorCode="" OR lower(errorCode)="success"

    | eval policy_raw='requestParameters.assumeRolePolicyDocument',
           role_name='requestParameters.roleName',
           role_arn='responseElements.role.arn',
           user_arn=coalesce(user_arn, 'userIdentity.arn'),
           src=coalesce(src, sourceIPAddress)

    | where isnotnull(policy_raw)

    | eval policy=if(match(policy_raw, "(?i)^%7b"), urldecode(policy_raw), policy_raw)

    | rex field=role_arn "^arn:[^:]+:iam::(?<account_from_role_arn>[0-9]{12}):role/"

    | eval role_account_id=coalesce(account_from_role_arn, recipientAccountId)
    | eval statements_json=json_extract(policy, "Statement")
    | eval statements=if(match(statements_json, "^\s*\["), json_array_to_mv(statements_json), statements_json)

    | mvexpand statements

    | eval effect=json_extract(statements, "Effect"),
           actions_json=json_extract(statements, "Action"),
           principal_json=json_extract(statements, "Principal")

    | eval actions=if(match(actions_json, "^\s*\["), json_array_to_mv(actions_json), actions_json)

    | mvexpand actions
    | mvexpand actions

    | eval trust_action=actions
    | eval action_pattern=replace(replace(lower(trust_action), "[*]", "%"), "[?]", "_")

    | where effect="Allow" AND like("sts:assumerole", action_pattern)
    | eval aws_json=json_extract(principal_json, "AWS")

    | eval principals=case(
        match(aws_json, "^\s*\["), json_array_to_mv(aws_json),
        isnotnull(aws_json), aws_json,
        principal_json="*", "*"
      )

    | mvexpand principals

    | eval trust_principal=principals

    | rex field=trust_principal "^arn:[^:]+:iam::(?<principal_account_from_arn>[0-9]{12}):"

    | eval principal_account_id=if(
        match(trust_principal, "^[0-9]{12}$"),
        trust_principal,
        principal_account_from_arn
      )

    | eval trust_type=case(
        trust_principal="*", "wildcard",
        isnotnull(role_account_id) AND isnotnull(principal_account_id)
          AND principal_account_id!=role_account_id, "external"
      )

    | where trust_type="wildcard" OR trust_type="external"

    | eval event_key=coalesce(eventID, md5(_raw))

    | dedup event_key trust_principal

    | table _time eventID user_arn src role_name role_arn role_account_id
            trust_type trust_principal principal_account_id trust_action

    | `aws_iam_role_created_with_external_trust_filter`
how_to_implement: You must install Splunk AWS Add on and enable CloudTrail logs in your AWS Environment.
known_false_positives: |-
    Approved cross-account integrations and deliberately configured wildcard trust may match.
    Review the role owner, trust-policy conditions, role permissions, and whether the principal is expected.
references:
    - https://docs.aws.amazon.com/IAM/latest/APIReference/API_CreateRole.html
    - https://stratus-red-team.cloud/attack-techniques/aws/aws.persistence.iam-backdoor-role/
finding:
    title: The AWS IAM role [$role_name$] created with [$trust_type$] trust
    entity:
        field: user_arn
        type: user
        score: 50
threat_objects:
    - field: src
      type: ip_address
analytic_story:
    - AWS IAM Privilege Escalation
asset_type: AWS Account
mitre_attack_id:
    - T1098.003
product:
    - Splunk Cloud
    - Splunk Enterprise
    - Splunk Enterprise Security
category: cloud
security_domain: threat
tests:
    - name: True Positive Test
      attack_data:
        - data: https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1098.003/aws_iam_role_created_with_external_trust/aws_iam_role_created_with_external_trust.json
          sourcetype: aws:cloudtrail
          source: aws_cloudtrail
      test_type: unit

Detection rules belong to the projects that publish them and remain under their own licenses. This site indexes and links to them; it claims no rights in them.