AWS Lateral Movement from Kubernetes SA via AssumeRoleWithWebIdentity
editAWS Lateral Movement from Kubernetes SA via AssumeRoleWithWebIdentity
editDetects 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.
Rule type: esql
Rule indices: None
Severity: high
Risk score: 73
Runs every: 1h
Searches indices from: now-24h (Date Math format, see also Additional look-back time)
Maximum alerts per execution: 100
References:
Tags:
- Domain: Cloud
- Data Source: AWS
- Data Source: Amazon Web Services
- Data Source: AWS CloudTrail
- Data Source: AWS IAM
- Data Source: AWS STS
- Use Case: Threat Detection
- Tactic: Lateral Movement
- Tactic: Discovery
- Tactic: Credential Access
- Resources: Investigation Guide
Version: 1
Rule authors:
- Elastic
Rule license: Elastic License v2
Investigation guide
editTriage 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
Rule query
editFROM logs-aws.cloudtrail-*
| WHERE (event.action == "AssumeRoleWithWebIdentity" AND user.name like "system:serviceaccount:*")
// S3 PutObject/GetObject is too common in legit pod SA behavior
OR (event.action IN ("ListBuckets", "DescribeInstances", "GetCallerIdentity",
"ListUsers", "ListRoles", "ListAttachedRolePolicies", "GetRolePolicy",
"GetSecretValue", "ListSecrets",
"GetParameters", "DescribeParameters", "ListKeys", "Decrypt",
"ListFunctions", "GetAuthorizationToken",
"SendCommand", "StartSession",
"CreateUser", "CreateAccessKey", "AttachRolePolicy", "CreateRole",
"PutRolePolicy", "UpdateAssumeRolePolicy",
"UpdateFunctionCode", "UpdateFunctionConfiguration", "ModifyInstanceAttribute",
"StopLogging", "DeleteTrail")
AND aws.cloudtrail.user_identity.type == "AssumedRole")
| GROK aws.cloudtrail.response_elements "accessKeyId=%{NOTSPACE:issued_key_id},"
| EVAL access_key = COALESCE(issued_key_id, aws.cloudtrail.user_identity.access_key_id)
| EVAL is_assume = CASE(event.action == "AssumeRoleWithWebIdentity", 1, 0)
| EVAL is_post_exploit = CASE(event.action != "AssumeRoleWithWebIdentity", 1, 0)
| EVAL phase = CASE(
event.action == "AssumeRoleWithWebIdentity", "initial_access",
event.action IN ("ListBuckets", "DescribeInstances", "ListUsers", "ListRoles",
"GetCallerIdentity", "ListAttachedRolePolicies", "GetRolePolicy",
"ListFunctions"), "recon",
event.action IN ("GetSecretValue", "ListSecrets", "GetParameters",
"GetAuthorizationToken", "Decrypt"), "credential_access",
event.action IN ("SendCommand", "StartSession"), "lateral_movement",
event.action IN ("CreateUser", "CreateAccessKey", "AttachRolePolicy",
"CreateRole", "PutRolePolicy", "UpdateAssumeRolePolicy",
"UpdateFunctionCode", "UpdateFunctionConfiguration",
"ModifyInstanceAttribute"), "persistence",
event.action IN ("StopLogging", "DeleteTrail"), "defense_evasion"
)
| STATS
Esql.assume_count = SUM(is_assume),
Esql.post_exploit_count = COUNT_DISTINCT(event.action),
Esql.attack_phases = VALUES(phase),
Esql.event_action_values = VALUES(event.action),
Esql.source_ip_values = VALUES(source.ip),
Esql.source_as_organization_name_values = VALUES(source.as.organization.name),
Esql.user_name_values = VALUES(user.name),
Esql.user_agent_original_values = VALUES(user_agent.original),
Esql.cloud_account_id_values = VALUES(cloud.account.id),
Esql.data_stream_namespace_values = VALUES(data_stream.namespace),
Esql.first_seen = MIN(@timestamp),
Esql.last_seen = MAX(@timestamp),
Esql.total_calls = COUNT(*)
BY access_key
| WHERE access_key is not null and Esql.assume_count >= 1 AND Esql.post_exploit_count >= 3
| EVAL aws.cloudtrail.user_identity.access_key_id = MV_FIRST(access_key)
| KEEP aws.cloudtrail.user_identity.access_key_id, Esql.*
Framework: MITRE ATT&CKTM
-
Tactic:
- Name: Lateral Movement
- ID: TA0008
- Reference URL: https://attack.mitre.org/tactics/TA0008/
-
Technique:
- Name: Use Alternate Authentication Material
- ID: T1550
- Reference URL: https://attack.mitre.org/techniques/T1550/
-
Sub-technique:
- Name: Application Access Token
- ID: T1550.001
- Reference URL: https://attack.mitre.org/techniques/T1550/001/
-
Technique:
- Name: Remote Services
- ID: T1021
- Reference URL: https://attack.mitre.org/techniques/T1021/
-
Sub-technique:
- Name: Cloud Services
- ID: T1021.007
- Reference URL: https://attack.mitre.org/techniques/T1021/007/
-
Tactic:
- Name: Discovery
- ID: TA0007
- Reference URL: https://attack.mitre.org/tactics/TA0007/
-
Technique:
- Name: Cloud Service Discovery
- ID: T1526
- Reference URL: https://attack.mitre.org/techniques/T1526/
-
Tactic:
- Name: Credential Access
- ID: TA0006
- Reference URL: https://attack.mitre.org/tactics/TA0006/
-
Technique:
- Name: Credentials from Password Stores
- ID: T1555
- Reference URL: https://attack.mitre.org/techniques/T1555/
-
Sub-technique:
- Name: Cloud Secrets Management Stores
- ID: T1555.006
- Reference URL: https://attack.mitre.org/techniques/T1555/006/