Incident Response advanced

Cloud Lateral Movement: The Techniques Your SOC Keeps Missing

Lateral movement moved to the cloud control plane. Each hop is individually authorized, so single-event rules miss the chain.

· 16 min read · Gowthamaraj Rajendran

Ask most SOCs where they watch for lateral movement and they will point at east-west network traffic: SMB between workstations, RDP into servers, a Zeek sensor on a segment boundary, an EDR rule for remote service creation. All of it is aimed at the network. And all of it is aimed at the wrong place, because the attacker moving through your cloud estate never touches a network boundary you instrument.

Cloud lateral movement happens in the control plane. An attacker who holds one set of credentials does not scan for the next host and pivot over TCP. They call an API. sts:AssumeRole into a second role. generateAccessToken to impersonate a service account. Mint a managed-identity token off the metadata service. Every one of those is an authenticated, authorized API call that lands in CloudTrail, Azure Activity, or Cloud Audit Logs, not in your netflow. The movement is real, the telemetry exists, and the network sensors you spent the budget on are structurally blind to it.

This is the concrete cloud instantiation of the argument I made in Living Off the Land in 2026: stop detecting the single event, detect the sequence. I am not going to re-argue it here. I am going to show you exactly what the sequence looks like across the three major clouds, at the level of specific API names and log fields, because the identity series has been conceptual and this is the hands-on version. The moat is parity: an attacker who is fluent in all three clouds is betting your detection team is fluent in only one.


Every Hop Is Individually Authorized: That Is the Whole Problem

The reason single-event rules miss this is not that the events are hidden. It is that each event, examined alone, is a legitimate authorized action. A role assuming another role is how CI/CD works. A service account minting a token for another service account is how half of GCP’s own tooling works. A VM reading its managed-identity token off the metadata endpoint is how every Azure SDK call authenticates. There is no single API call in a control-plane lateral-movement chain that is anomalous on its face, because each hop is exactly the primitive the platform built for automation.

Attackers reach the control plane the same way they always have: they skip the network stages entirely. As I argued in the identity kill chain, the modern intrusion does not exploit its way host-to-host. It authenticates. The roles, service accounts, and managed identities they land on are non-human identities, the detection blind spot that outnumbers your humans and carries standing privilege nobody reviews. Once an attacker holds one of them, movement is a sequence of authorized calls. The detection has to key on the sequence and its novelty, not on any one call, because there is no bad call to find.

Lead with prevention on each cloud. Then detect what prevention leaves behind. That order matters: every control you enforce becomes the tripwire that fires when an attacker steps over it.


AWS: Role Chaining Is the Native Lateral-Movement Primitive

AWS is where this is most mature and most documented, so start here.

Prevent first. Three controls shrink the surface before any detection runs:

  • Enforce IMDSv2. The instance metadata service is how an EC2 role credential gets stolen: an SSRF bug in an app makes the instance fetch its own credentials off the link-local endpoint 169.254.169.254 and hand them to the attacker [4]. IMDSv2’s session-token requirement neutralizes GET-based SSRF. Adoption is the problem: Datadog’s 2024 State of Cloud Security study found only 32% of EC2 instances have IMDSv2 enforced, and just 10% of instances older than a year, versus 42% for instances launched in the prior two weeks [3]. Set a hop limit of 1 and require IMDSv2 at the account level.
  • Scope role trust, don’t wildcard it. A trust policy that lets * or a whole org assume a role is a confused deputy. Pin the principal and, for federated roles, the exact subject claim.
  • Require sts:SourceIdentity and propagate it. Force an immutable source-identity string onto assumed sessions so a chained assume carries the original human’s identity through every hop. It converts an anonymous chain into an attributable one, and gives your detection a field to pivot on.

Then detect. The lateral movement itself is sts:AssumeRole, and the signal is not that it happened. It is who is calling it. Elastic’s AWS STS role-chaining rule keys on exactly the right fields [5]: event.provider is sts.amazonaws.com, event.action is AssumeRole, and critically aws.cloudtrail.user_identity.type is AssumedRole, meaning the caller is already a temporary session, not a human or root. A user assuming a role is a login. A role assuming a role is a hop. To classify cross-account, compare the calling account aws.cloudtrail.recipient_account_id against the target role’s account aws.cloudtrail.resources.account_id; when they differ, the session just walked across an account boundary. Elastic ships this as a new_terms rule that fires on the first observed issuer-to-target pairing, because the novel edge is the tell.

Role chaining is not the only AWS control-plane move, and the best-documented real-world chain does not even use STS chaining. Permiso’s teardown of LUCR-3 (the cloud arm of Scattered Spider) walks a different path [2]: the actor calls ReplaceIamInstanceProfileAssociation to swap a more-privileged instance profile onto an EC2 box they control, runs the AWS-GatherSoftwareInventory SSM job across every instance to inventory the estate, uses CloudShell to reach into Secrets Manager and harvest stored credentials, and drops persistence with CreateUser plus CreateAccessKey. Each call is an ordinary admin action. The chain (profile swap, fleet-wide inventory, secrets scrape, key creation) is a full intrusion. Build detections on the sequence: ReplaceIamInstanceProfileAssociation on an instance that has never had its profile changed, CreateAccessKey for a user created minutes earlier, GetSecretValue from a CloudShell session.


Azure: Managed-Identity Tokens and the Service-Principal-to-SYSTEM Pivot

Azure gives an attacker two distinct control-plane movement surfaces, and they compose.

Prevent first. Put a human gate on the permissions that matter:

  • PIM and second approval on high-risk app permissions. Any service principal holding DeviceManagementConfiguration.ReadWrite.All, RoleManagement.ReadWrite.Directory, or AppRoleAssignment.ReadWrite.All should require Privileged Identity Management activation with a second approver. These are directory-takeover permissions; they should not sit standing on an app registration.
  • Conditional Access and least privilege on managed identities. The managed identity on a VM should hold only the resource scopes it needs, so a stolen token buys the minimum.

Then detect. The first surface is token minting. An Azure VM’s managed identity is retrieved off the same metadata endpoint pattern as AWS: the SDK requests an OAuth2 token for a target resource, and an attacker on the box can request the same token for Azure Resource Manager, Key Vault, or Graph. This is the managed-identity home-address problem: the token is bound to one VM, so its use from anywhere else is theft, and the detection is deviation from where the identity lives. Datadog’s Entra lateral-movement research documents the on-host precursors too (cached service-principal secrets sitting in the ~/.azure directory, and pass-the-PRT to import a primary refresh token cookie into an attacker session) followed by the service principal authenticating and reaching into Key Vault [6].

The second surface is nastier because it crosses from cloud control plane down to endpoint SYSTEM. Mandiant documented a service principal holding DeviceManagementConfiguration.ReadWrite.All abusing Intune to run code on every managed device [7]. The technique is a single Graph call: PATCH the deviceManagementScripts endpoint to replace a device-management script’s body, set "runAsAccount": "system" and "enforceSignatureCheck": false, and the payload executes as NT AUTHORITY\SYSTEM on every enrolled device at next check-in. A cloud identity just became SYSTEM on your entire fleet, and not one packet of it looks like lateral movement to a network sensor. Detect on the Graph audit events: a deviceManagementScripts create or update, especially from a service principal rather than an interactive admin, is a high-signal event with near-zero legitimate volume.


GCP: Service-Account Impersonation Is a Logged, Ignorable Line

GCP’s control-plane movement is service-account impersonation, and it is the cleanest of the three to reason about because Google logs it explicitly.

Prevent first. The entire technique rests on one role. roles/iam.serviceAccountTokenCreator (Service Account Token Creator) grants the iam.serviceAccounts.getAccessToken permission, which is all an attacker needs to mint credentials for another service account [8]. Audit who holds Token Creator, strip broad grants, and use IAM Conditions to bound impersonation to specific target service accounts. A project where any principal can impersonate any service account is a project where one foothold reaches everything.

Then detect. Impersonation calls generateAccessToken (and generateIdToken) on the IAM Credentials API, and Cloud Audit Logs record both identities: the caller and the service account being impersonated. The documentation is explicit: “most audit logs include both their identity and the identity of the service account they’re impersonating” [8]. That gives you a clean detection: a principal calling GenerateAccessToken where the target service account is one it has never impersonated before, or where the caller is itself a service account (impersonation chaining). Baseline the caller-to-target impersonation graph and alert on new edges: the same novel-edge logic as AWS role chaining, applied to a different API. The line is sitting in your log. The question is whether anything reads it.


The Seam Between Domains Is What No Single Product Owns

Line the three clouds up and the parity is the point: role chaining, token minting, impersonation: the same movement pattern, three different API vocabularies. But the most dangerous version isn’t inside any one cloud. It’s the seam between domains: email to cloud management to VM to endpoint.

That is precisely how Scattered Spider operates, and it is why they are hard to catch. CrowdStrike’s 2024 Threat Hunting Report, analyzing their cross-domain intrusions, found that “the detectable footprint of this activity in any single detection domain was very low and difficult to conclusively identify” [1]. Each domain sees a sliver: identity sees a normal federated login, the cloud sees an authorized role assume, the endpoint sees a signed script from Intune. No single sensor sees enough to fire. The same report measured cloud intrusions up 75% and cloud-conscious cases up 110% year over year: the adversary is investing in exactly this surface [1]. Storm-0558’s cross-tenant token forgery is the archetype of the seam: a signing-key compromise let a single actor mint tokens across the boundary between identity and mail [9], the same authorization-layer abuse I covered under OAuth token theft. The chain crosses products. Your detection has to as well.


The Steelman: “The CSPs Already Ship This for Free”

Here is the strongest objection, and it deserves an evidence-backed answer, not a wave.

“GuardDuty, Defender for Cloud, and Security Command Center already flag anomalous role assumption and impersonation. Building custom chain detection reinvents a wheel the cloud provider ships free.”

The provider tooling is real and you should run it. GuardDuty ships PrivilegeEscalation:IAMUser/AnomalousBehavior, Discovery:IAMUser/AnomalousBehavior, and the whole AnomalousBehavior family, plus InstanceCredentialExfiltration.OutsideAWS for stolen instance credentials [10]. That covers a lot. But read how those findings are generated, because the mechanism is the limit.

Every AnomalousBehavior finding is produced by “GuardDuty’s anomaly detection machine learning (ML) model,” which scores an API request against a learned baseline of “the user that made the request, the location the request was made from, and the specific API that was requested” [10]. Two consequences fall straight out of that design. First, it is anomaly-scored within a single service boundary and a single account’s learned baseline: it evaluates whether an API call is unusual for that identity, not whether a sequence of individually-normal calls spans a domain seam it has no view into. Cross-service and cross-domain movement is normal for each service in isolation, so each hop scores benign. Second (and AWS states this plainly for the credential-exfiltration findings) once the model “observes continued activity from a remote host, its machine learning (ML) model will identify this as an expected behavior” and “stop generating this finding” [10]. The model learns and quiets. It also degrades on first-observed novel paths, because a baseline model is weakest exactly where there is no baseline, and the attacker’s path is, by definition, one you have never seen.

So the honest answer: run GuardDuty, Defender for Cloud, and SCC as your anomaly floor. But their view stops at the edge of one service in one account, tuned to one learned baseline, and the movement that hurts you is the novel edge across the seam between email, cloud management, and the endpoint, which no single-CSP product owns because no single-CSP product can see all three. Custom chain detection is not reinventing their wheel. It is covering the ground their wheel does not touch.


One Rule: Cross-Account Role Chaining From an Already-Assumed Session

Here is the AWS chain hop as a Sigma rule. It is deliberately not “someone assumed a role”. That fires all day. It fires only when the caller is already a temporary session and the target role lives in a different account: the exact shape of a stolen session walking across an account boundary, which a first login can never be.

# Detection rationale: sts:AssumeRole is the native AWS lateral-movement primitive,
# but a user or root assuming a role is a normal LOGIN and must not fire. The
# lateral-movement signal is a role assuming ANOTHER role, i.e. the caller is
# already an assumed-role session (userIdentity.type == AssumedRole), AND the
# target role is in a DIFFERENT account than the one recording the event
# (resources[].accountId != recipientAccountId). That combination is a stolen or
# chained session crossing an account boundary, not a first authentication. It keys
# on the HOP inside an existing session, not on the assume itself.
title: AWS STS Cross-Account Role Chaining From an Already-Assumed Session
status: experimental
logsource:
  product: aws
  service: cloudtrail
detection:
  assumed_caller:
    eventSource: 'sts.amazonaws.com'
    eventName: 'AssumeRole'
    userIdentity.type: 'AssumedRole'   # caller is a temp session, not a user/root
  successful:
    responseElements|exists: true       # the assume returned credentials
  # Native Sigma cannot compare two event fields, so an enrichment step sets
  # tdl_cross_account = true when resources[].accountId != recipientAccountId.
  # Do the accountId inequality in the SIEM/pipeline, then key on the flag here.
  cross_account_hop:
    tdl_cross_account: true
  condition: assumed_caller and successful and cross_account_hop
falsepositives:
  - CI/CD deploy roles that legitimately chain cross-account (Terraform, CodePipeline,
    Spacelift): allowlist the known issuer -> target role ARN pairs and alert only
    on NOVEL pairs, the way a new_terms rule does
  - Federated SSO landing roles that fan out into per-account roles right after login
  - Cross-account monitoring, backup, or cost-tooling roles: allowlist and enrich
    with sts:SourceIdentity so a legitimate chain carries its human's identity through
level: high
tags:
  - attack.lateral_movement
  - attack.t1550.001   # Use Alternate Authentication Material: Application Access Token
  - attack.t1078.004   # Valid Accounts: Cloud Accounts

The rule’s precision comes entirely from the two qualifiers. Drop the AssumedRole caller check and it fires on every SSO login. Drop the cross-account comparison and it fires on every legitimate in-account role fan-out. Keep both, allowlist your known issuer-to-target pairs, and what is left is the novel cross-account hop: the edge the CSP’s baseline model is weakest on and the attacker relies on being unseen.


Run This Monday

Three moves, in order, this week:

  1. Turn on the account-boundary check you already have data for. You ingest CloudTrail. Write the rule above (AssumeRole where userIdentity.type is AssumedRole and the target account differs from the recipient account), allowlist your CI/CD and SSO pairs, and alert on novel edges. Do the GCP twin: GenerateAccessToken to a service account the caller has never impersonated.
  2. Enforce IMDSv2 and pull the number. Query how many of your EC2 instances actually enforce IMDSv2. If you are near the 32% average, every instance below that line is one SSRF bug from leaking a role credential into a chain. Set the account-level default and a hop limit of 1.
  3. Alarm the SP-to-SYSTEM pivot. Add one detection for a deviceManagementScripts create or update from a service principal, and gate DeviceManagementConfiguration.ReadWrite.All behind PIM with a second approver. That single Graph call turns a cloud identity into SYSTEM on your whole fleet.

Your network sensors were never going to see any of this. The movement is in the control plane, in logs you already store, expressed as API calls each of which is individually authorized. The attacker’s entire bet is that you are watching for a pivot over the wire while they chain roles, mint tokens, and impersonate service accounts one authorized call at a time. Detect the chain, not the call.

Resources

  1. How CrowdStrike Hunts, Identifies, and Defeats Cloud-Focused Threats. CrowdStrike, 2024 (2024 Threat Hunting Report: Scattered Spider cross-domain footprint in any single detection domain “very low and difficult to conclusively identify”; cloud intrusions +75%, cloud-conscious cases +110% year over year).
  2. LUCR-3: Scattered Spider Getting SaaS-y in the Cloud. Permiso (AWS technique chain: ReplaceIamInstanceProfileAssociation, AWS-GatherSoftwareInventory SSM job, CloudShell to Secrets Manager, CreateUser + CreateAccessKey).
  3. Key Learnings from the 2024 State of Cloud Security Study. Datadog (32% of EC2 instances enforce IMDSv2; only 10% for instances older than a year, versus 42% for those launched in the prior two weeks).
  4. Stealing EC2 Instance Role Credentials. Datadog Security Labs (SSRF against the metadata service at 169.254.169.254 to exfiltrate instance-role credentials).
  5. AWS STS Role Chaining. Elastic (detection on session_issuer.arn, resources.arn, user_identity.type = AssumedRole, and recipient_account_id vs resources.account_id cross-account comparison; new_terms on first-observed issuer-to-target pair).
  6. Lateral Movement in Entra ID and Azure. Datadog (cached service-principal secrets in ~/.azure, pass-the-PRT, service principal reaching Key Vault).
  7. Abusing Intune Permissions for Lateral Movement in Entra ID Environments. Google Cloud / Mandiant (DeviceManagementConfiguration.ReadWrite.All service principal PATCHes deviceManagementScripts with runAsAccount: system to execute as NT AUTHORITY\SYSTEM on managed devices).
  8. Service Account Impersonation. Google Cloud (iam.serviceAccounts.getAccessToken via roles/iam.serviceAccountTokenCreator; audit logs record both caller and impersonated identity).
  9. Analysis of Storm-0558 Techniques for Unauthorized Email Access. Microsoft, 2023 (cross-tenant token forgery via a compromised signing key, archetype of cross-domain seam abuse).
  10. GuardDuty IAM Finding Types. AWS (AnomalousBehavior findings are ML anomaly-scored per identity/baseline; the model learns continued remote-host activity as “expected” and stops generating the finding).