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 isAssumeRoleWithWebIdentitywith a Kubernetes service account inEsql.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, orRunInstancesalongside 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/audinrequest_parametersandresources. - In Kubernetes: map
Esql.user_name_valuesto namespace and workload; check audit logs aroundEsql.first_seenforexec, 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_callsand IAM impact ofEsql.event_action_valuesto 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
- AWS Credentials Used from GitHub Actions and Non-CI/CD Infrastructure
- AWS Account Discovery By Rare User
- AWS IAM Long-Term Access Key First Seen from Source IP
- AWS IAM Long-Term Access Key Correlated with Elevated Detection Alerts
- AWS EC2 Instance Console Login via Assumed Role