Investigating a cloud identity takeover with federated search across Amazon S3 and Snowflake
This article is one of three investigation examples in a series. See Investigating security incidents across Amazon S3 and Snowflake with federated search for the reference architecture, dataset list, and prerequisites used in this example.
An account authenticated to a production host from outside the corporate network and was then refused sudo access twice. The host prevented immediate escalation, but that doesn't mean the session was harmless. The next question is whether the identity moved into cloud APIs where the host's controls couldn't stop it.
How federated search in Splunk Cloud Platform helps with this use case
Step 1: Is this address normal for the account?
Use an index in Splunk Cloud Platform as the source.
search index=linux_secure sourcetype=linux_secure "Accepted password for fgupta"
| rex field=_raw "from (?<client_ip>[0-9.]+) port"
| eval scope=if(cidrmatch("10.0.0.0/8", client_ip), "corporate", "external")
| stats count() AS logins, dc(client_ip) AS addresses BY scope
This search extracts the source address from the accepted-login event, classifies it as corporate or external, and counts logins and distinct addresses.
The expected shape is approximately 155 logins from the corporate range and one login from outside it. The external address is 198.51.100.77.
The search results show the account's corporate and external login scope:

You should treat the external login as an investigation pivot, not as proof of compromise. Continue by reconstructing the session and looking for activity from the same identity and address.
Step 2: What did the session do on the host?
Use an index in Splunk Cloud Platform as the source.
index=linux_secure sourcetype=linux_secure "fgupta" | where like(_raw, "%NOT in sudoers%") OR like(_raw, "%Accepted password%") | sort _time | table _time, host, _raw
The sequence shows the login from 198.51.100.77, followed by two refused attempts to run /bin/cat /etc/shadow and /usr/bin/passwd root. The host held, but the sequence suggests post-authentication discovery and attempted credential or privilege manipulation.

You should preserve the source address, timestamp, host, and account as pivots for the cloud-history search. A blocked sudo action is still useful evidence when it's followed by activity in another control plane.
Step 3: Where did the session go instead?
Use Amazon S3 as the source.
FROM s3_cloudtrail WHERE userIdentity="fgupta" AND sourceIPAddress="198.51.100.77" AND earliest=-90d | eval when=strftime(_time, "%F %T"), err=coalesce(errorCode, "-") | fields when, eventSource, eventName, err | sort when
The search returns 11 calls in eight minutes. Eight target iam.amazonaws.com, a service this account never touches from the corporate range. The sequence includes:
GetCallerIdentityandListUsersto orient in the account.CreateAccessKeyto establish persistence.AttachUserPolicyto widen privileges.CreateLoginProfileto enable console access.DeactivateMFADeviceto remove a second factor.AssumeRoleto move into another role.
Two calls were refused: PutUserPolicy and DeactivateMFADevice returned AccessDenied.

You should escalate this as a likely account takeover incident for immediate attention. Disable or rotate the exposed credentials, revoke newly created access keys and sessions, inspect attached policies and role assumptions, and review the same source address across other identities and accounts.
Step 4: Who owns this account, and was any of this part of their job?
Use Amazon S3, Snowflake, and an index in Splunk Cloud Platform as the sources.
This search joins the three sources: SSH activity from the index, CloudTrail activity from Amazon S3, and the HR record from Snowflake.
FROM s3_cloudtrail WHERE userIdentity="fgupta" AND sourceIPAddress="198.51.100.77" AND earliest=-90d | eval refused=if(isnotnull(errorCode), 1, 0) | stats count() AS api_calls, dc(eventName) AS apis, sum(refused) AS denied BY userIdentity, sourceIPAddress | join type=inner left=L right=R where L.sourceIPAddress=R.src_ip [search index=linux_secure sourcetype=linux_secure "Accepted password for fgupta" "198.51.100.77" | stats count() AS ssh_logins | eval src_ip="198.51.100.77"] | eval account='L.userIdentity', ip='L.sourceIPAddress', api_calls='L.api_calls', apis='L.apis', denied='L.denied', ssh_logins='R.ssh_logins' | fields account, ip, ssh_logins, api_calls, apis, denied | join type=inner left=A right=B where A.account=B.OS_ACCOUNT [FROM sf_employee_status | fields OS_ACCOUNT, FULL_NAME, DEPARTMENT, ROLE, CLEARANCE_LEVEL, EMPLOYMENT_STATUS] | eval who='B.FULL_NAME', dept='B.DEPARTMENT', job='B.ROLE', clearance='B.CLEARANCE_LEVEL', account='A.account', ip='A.ip', ssh_logins='A.ssh_logins', api_calls='A.api_calls', denied='A.denied' | fields who, dept, job, clearance, account, ip, ssh_logins, api_calls, denied
The result identifies Farid Gupta in IT Operations, with an Analyst role and standard clearance: one SSH login, 11 API calls, and two denied calls.

An analyst account creating access keys and attaching IAM policies is not merely a permissions question. It's a high-confidence mismatch between identity context and observed behavior. Neither the host logs nor CloudTrail alone could establish that mismatch.
Using this information, the SOC can move from an ambiguous external login to an attributable, evidence-backed incident without waiting for a warehouse export or asking the analyst to authenticate to Snowflake.
You should contain the identity, revoke persistence mechanisms, validate MFA and role trust relationships, search for follow-on activity, and document the Snowflake employee record as part of the incident evidence.
Next steps
Continue with the next investigation example in this series: Scoping a password spray breach with federated search across Amazon S3 and Snowflake.

