AWS Lateral Movement from Kubernetes SA via AssumeRoleWithWebIdentity

Detects when credentials issued through AssumeRoleWithWebIdentity for a Kubernetes service account identity are later used for several distinct AWS control-plane actions on the same session access key. Workloads that use EKS IAM Roles for Service Accounts routinely exchange a projected service-account token for short-lived IAM credentials; this rule highlights sessions where that exchange is followed by a spread of sensitive APIs—reconnaissance, secrets and parameter access, IAM changes, or compute creation—beyond what routine pod traffic usually shows. High-volume S3 object reads and writes are excluded from the correlation set to reduce noise from normal data-plane work.

Elastic rule (View on GitHub)

  1[metadata]
  2creation_date = "2026/04/22"
  3integration = ["aws"]
  4maturity = "production"
  5updated_date = "2026/04/22"
  6
  7[rule]
  8author = ["Elastic"]
  9description = """
 10Detects when credentials issued through `AssumeRoleWithWebIdentity` for a Kubernetes service account identity are later
 11used for several distinct AWS control-plane actions on the same session access key. Workloads that use EKS IAM Roles
 12for Service Accounts routinely exchange a projected service-account token for short-lived IAM credentials; this rule
 13highlights sessions where that exchange is followed by a spread of sensitive APIs—reconnaissance, secrets and parameter
 14access, IAM changes, or compute creation—beyond what routine pod traffic usually shows. High-volume S3 object reads and
 15writes are excluded from the correlation set to reduce noise from normal data-plane work.
 16"""
 17false_positives = [
 18    """
 19    In-cluster automation may produce the same pattern: validate `Esql.user_name_values`, workload ownership, and
 20    whether `Esql.source_ip_values` / `Esql.source_asn_names` match expected egress before tuning or allowlisting.
 21    """,
 22]
 23from = "now-24h"
 24interval = "1h"
 25language = "esql"
 26license = "Elastic License v2"
 27name = "AWS Lateral Movement from Kubernetes SA via AssumeRoleWithWebIdentity"
 28note = """## Triage and analysis
 29
 30### Investigating AWS Lateral Movement from Kubernetes SA via AssumeRoleWithWebIdentity
 31
 32The rule output is already aggregated per session key. Start from **`aws.cloudtrail.user_identity.access_key_id`**, then
 33use the bundled fields to scope time, identity, and network context before drilling into raw CloudTrail.
 34
 35**What to review first**
 36
 37- **`Esql.first_seen` / `Esql.last_seen`**: time window for the whole session; pull raw CloudTrail for this key between
 38  those timestamps and confirm ordering (assume before follow-ons).
 39- **`Esql.assume_count`**: should be at least 1; verify the assume row is `AssumeRoleWithWebIdentity` with a Kubernetes
 40  service account in **`Esql.user_name_values`** (`system:serviceaccount:*`).
 41- **`Esql.post_exploit_count`**, **`Esql.event_action_values`**, **`Esql.attack_phases`**: which distinct APIs fired on the
 42  same key; flag unexpected IAM, secrets, or `RunInstances` alongside recon.
 43- **`Esql.total_calls`**: volume beyond “three distinct actions”—helps separate quick probes from sustained abuse.
 44- **`Esql.source_ip_values`**, **`Esql.source_asn_names`**, **`Esql.user_agent_values`**: compare to known cluster egress,
 45  NAT, or approved automation; divergent ASNs or clients can indicate token use off-cluster.
 46
 47**Next pivots**
 48
 49- In CloudTrail assume events for this key: role ARN, OIDC provider, and `sub` / `aud` in `request_parameters` and
 50  `resources`.
 51- In Kubernetes: map `Esql.user_name_values` to namespace and workload; check audit logs around `Esql.first_seen` for
 52  `exec`, secret reads, or new RBAC.
 53
 54### False positive analysis
 55
 56- In-cluster operators (GitOps, scanners, backups) can still satisfy the distinct-action bar; validate workload image,
 57  schedule, and approved IRSA role scope.
 58- Sessions that barely exceed the distinct-action threshold: use **`Esql.total_calls`** and IAM impact of
 59  **`Esql.event_action_values`** to decide urgency.
 60
 61### Response and remediation
 62
 63- Revoke or constrain the IAM role session; tighten OIDC trust conditions; rotate or patch the affected workload; reduce
 64  service account permissions and egress where abuse is confirmed.
 65
 66### Additional information
 67
 68- [IAM OIDC identity provider](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc.html)
 69- [EKS IAM roles for service accounts](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html)
 70- [AssumeRoleWithWebIdentity](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html)
 71"""
 72references = [
 73    "https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html",
 74    "https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html",
 75]
 76risk_score = 73
 77rule_id = "a1b2c3d4-e5f6-4789-a0b1-c2d3e4f5a6b7"
 78severity = "high"
 79tags = [
 80    "Domain: Cloud",
 81    "Data Source: AWS",
 82    "Data Source: Amazon Web Services",
 83    "Data Source: AWS CloudTrail",
 84    "Data Source: AWS IAM",
 85    "Data Source: AWS STS",
 86    "Use Case: Threat Detection",
 87    "Tactic: Lateral Movement",
 88    "Tactic: Discovery",
 89    "Tactic: Credential Access",
 90    "Resources: Investigation Guide",
 91]
 92timestamp_override = "event.ingested"
 93type = "esql"
 94
 95query = '''
 96FROM logs-aws.cloudtrail-*
 97| WHERE (event.action == "AssumeRoleWithWebIdentity" AND user.name like "system:serviceaccount:*")
 98  // S3 PutObject/GetObject is too  common in legit pod SA behavior 
 99  OR (event.action IN ("ListBuckets", "DescribeInstances", "GetCallerIdentity",
100    "ListUsers", "ListRoles", "ListAttachedRolePolicies", "GetRolePolicy",
101    "GetSecretValue", "ListSecrets",
102    "GetParameters", "DescribeParameters", "ListKeys", "Decrypt",
103    "ListFunctions", "GetAuthorizationToken",
104    "SendCommand", "StartSession",
105    "CreateUser", "CreateAccessKey", "AttachRolePolicy", "CreateRole",
106    "PutRolePolicy", "UpdateAssumeRolePolicy",
107    "UpdateFunctionCode", "UpdateFunctionConfiguration", "ModifyInstanceAttribute",
108    "StopLogging", "DeleteTrail")
109    AND aws.cloudtrail.user_identity.type == "AssumedRole")
110| GROK aws.cloudtrail.response_elements "accessKeyId=%{NOTSPACE:issued_key_id},"
111| EVAL access_key = COALESCE(issued_key_id, aws.cloudtrail.user_identity.access_key_id)
112| EVAL is_assume = CASE(event.action == "AssumeRoleWithWebIdentity", 1, 0)
113| EVAL is_post_exploit = CASE(event.action != "AssumeRoleWithWebIdentity", 1, 0)
114| EVAL phase = CASE(
115    event.action == "AssumeRoleWithWebIdentity", "initial_access",
116    event.action IN ("ListBuckets", "DescribeInstances", "ListUsers", "ListRoles",
117      "GetCallerIdentity", "ListAttachedRolePolicies", "GetRolePolicy",
118      "ListFunctions"), "recon",
119    event.action IN ("GetSecretValue", "ListSecrets", "GetParameters",
120      "GetAuthorizationToken", "Decrypt"), "credential_access",
121    event.action IN ("SendCommand", "StartSession"), "lateral_movement",
122    event.action IN ("CreateUser", "CreateAccessKey", "AttachRolePolicy",
123      "CreateRole", "PutRolePolicy", "UpdateAssumeRolePolicy",
124      "UpdateFunctionCode", "UpdateFunctionConfiguration",
125      "ModifyInstanceAttribute"), "persistence",
126    event.action IN ("StopLogging", "DeleteTrail"), "defense_evasion"
127  )
128| STATS 
129    Esql.assume_count = SUM(is_assume),
130    Esql.post_exploit_count = COUNT_DISTINCT(event.action),
131    Esql.attack_phases = VALUES(phase),
132    Esql.event_action_values = VALUES(event.action),
133    Esql.source_ip_values = VALUES(source.ip),
134    Esql.source_as_organization_name_values = VALUES(source.as.organization.name),
135    Esql.user_name_values = VALUES(user.name),
136    Esql.user_agent_original_values = VALUES(user_agent.original),
137    Esql.cloud_account_id_values = VALUES(cloud.account.id),
138    Esql.data_stream_namespace_values = VALUES(data_stream.namespace),
139    Esql.first_seen = MIN(@timestamp),
140    Esql.last_seen = MAX(@timestamp),
141    Esql.total_calls = COUNT(*)
142  BY access_key
143| WHERE access_key is not null and Esql.assume_count >= 1 AND Esql.post_exploit_count >= 3
144| EVAL aws.cloudtrail.user_identity.access_key_id = MV_FIRST(access_key)
145| KEEP aws.cloudtrail.user_identity.access_key_id, Esql.*
146'''
147
148[rule.investigation_fields]
149field_names = [
150    "aws.cloudtrail.user_identity.access_key_id",
151    "Esql.assume_count",
152    "Esql.post_exploit_count",
153    "Esql.attack_phases",
154    "Esql.event_action_values",
155    "Esql.source_ip_values",
156    "Esql.source_as_organization_name_values",
157    "Esql.user_name_values",
158    "Esql.user_agent_original_values",
159    "Esql.first_seen",
160    "Esql.last_seen",
161    "Esql.total_calls",
162]
163
164[[rule.threat]]
165framework = "MITRE ATT&CK"
166
167[[rule.threat.technique]]
168id = "T1550"
169name = "Use Alternate Authentication Material"
170reference = "https://attack.mitre.org/techniques/T1550/"
171
172[[rule.threat.technique.subtechnique]]
173id = "T1550.001"
174name = "Application Access Token"
175reference = "https://attack.mitre.org/techniques/T1550/001/"
176
177[[rule.threat.technique]]
178id = "T1021"
179name = "Remote Services"
180reference = "https://attack.mitre.org/techniques/T1021/"
181
182[[rule.threat.technique.subtechnique]]
183id = "T1021.007"
184name = "Cloud Services"
185reference = "https://attack.mitre.org/techniques/T1021/007/"
186
187[rule.threat.tactic]
188id = "TA0008"
189name = "Lateral Movement"
190reference = "https://attack.mitre.org/tactics/TA0008/"
191
192[[rule.threat]]
193framework = "MITRE ATT&CK"
194
195[[rule.threat.technique]]
196id = "T1526"
197name = "Cloud Service Discovery"
198reference = "https://attack.mitre.org/techniques/T1526/"
199
200[rule.threat.tactic]
201id = "TA0007"
202name = "Discovery"
203reference = "https://attack.mitre.org/tactics/TA0007/"
204
205[[rule.threat]]
206framework = "MITRE ATT&CK"
207
208[[rule.threat.technique]]
209id = "T1555"
210name = "Credentials from Password Stores"
211reference = "https://attack.mitre.org/techniques/T1555/"
212
213[[rule.threat.technique.subtechnique]]
214id = "T1555.006"
215name = "Cloud Secrets Management Stores"
216reference = "https://attack.mitre.org/techniques/T1555/006/"
217
218[rule.threat.tactic]
219id = "TA0006"
220name = "Credential Access"
221reference = "https://attack.mitre.org/tactics/TA0006/"

Triage and analysis

Investigating AWS Lateral Movement from Kubernetes SA via AssumeRoleWithWebIdentity

The rule output is already aggregated per session key. Start from aws.cloudtrail.user_identity.access_key_id, then use the bundled fields to scope time, identity, and network context before drilling into raw CloudTrail.

What to review first

  • Esql.first_seen / Esql.last_seen: time window for the whole session; pull raw CloudTrail for this key between those timestamps and confirm ordering (assume before follow-ons).
  • Esql.assume_count: should be at least 1; verify the assume row is AssumeRoleWithWebIdentity with a Kubernetes service account in Esql.user_name_values (system:serviceaccount:*).
  • Esql.post_exploit_count, Esql.event_action_values, Esql.attack_phases: which distinct APIs fired on the same key; flag unexpected IAM, secrets, or RunInstances alongside recon.
  • Esql.total_calls: volume beyond “three distinct actions”—helps separate quick probes from sustained abuse.
  • Esql.source_ip_values, Esql.source_asn_names, Esql.user_agent_values: compare to known cluster egress, NAT, or approved automation; divergent ASNs or clients can indicate token use off-cluster.

Next pivots

  • In CloudTrail assume events for this key: role ARN, OIDC provider, and sub / aud in request_parameters and resources.
  • In Kubernetes: map Esql.user_name_values to namespace and workload; check audit logs around Esql.first_seen for exec, secret reads, or new RBAC.

False positive analysis

  • In-cluster operators (GitOps, scanners, backups) can still satisfy the distinct-action bar; validate workload image, schedule, and approved IRSA role scope.
  • Sessions that barely exceed the distinct-action threshold: use Esql.total_calls and IAM impact of Esql.event_action_values to decide urgency.

Response and remediation

  • Revoke or constrain the IAM role session; tighten OIDC trust conditions; rotate or patch the affected workload; reduce service account permissions and egress where abuse is confirmed.

Additional information

References

Related rules

to-top