CoursesAdvanced cloud securityCloud incident response & forensics

Cloud incident response & forensics

Isolation, snapshots, credential compromise, and runbooks.

Expert35 min · lesson 12 of 15

Cloud incident response is like investigating a break-in at a building whose rooms rearrange themselves every few minutes. The intruder carries a key that quietly copies itself. Brush the wrong switch and the whole crime scene gets bulldozed and rebuilt spotless. There is no server to unplug and carry down to a lab. Everything is an API call (a request your tools send to the provider over the network), every credential expires on a timer, and the evidence, the memory and disks and logs, keeps changing under you and quietly deletes itself.

So cloud forensics is a race, run through the same control plane you administer with (the provider's management interface, the thing your dashboards and scripts talk to), to freeze the state of things faster than your own automation wipes it. Three plain words hold the whole job together. Containment shrinks the blast radius (how far the attacker can reach). Eradication pulls out the attacker's footholds. Forensics preserves and reconstructs what really happened. The order is fixed: contain first, preserve second, understand third. And containment must never destroy the record.

Isolate the box, keep it breathing

This is why the reflex to terminate a hacked machine is almost always the wrong first move. Terminating throws away volatile memory: secrets sitting decrypted in RAM (a machine's fast, temporary memory that empties the moment it loses power), the malware's command-and-control settings (the configuration it uses to phone home to the attacker), processes injected straight into memory that never touched the disk. On a throwaway instance, terminating usually deletes the root disk too, erasing the only account of what was taken and whether the attacker left themselves a way back in. Isolation makes a box harmless. A snapshot makes it answerable. A terminated box is just gone.

Isolation is one idea spelled three ways. On AWS you drop the instance into a quarantine security group (a virtual firewall wrapped around the instance) that permits nothing, and you detach it from its load balancer and Auto Scaling group (the service that keeps a fixed number of healthy instances running). On Google Cloud you re-tag the virtual machine so a deny-all firewall rule you staged earlier latches onto it, or you strip its external IP address. On Azure you bind a lock-down network security group (NSG, Azure's per-resource firewall) to the network card. Every time, the box keeps running so its memory stays alive, while the network goes dark so the attacker is locked out. Detaching matters as much as denying. Leave the instance in its Auto Scaling group and the platform may spot an 'unhealthy' box and terminate it for you, shredding the evidence you were trying to hold.

terminal
# modify-instance-attribute is silent on success; the quarantine SG replaces ALL current SGs
$ aws ec2 modify-instance-attribute --instance-id i-0ab12cd34ef567890 --groups sg-0quarantine
# then pull the box out of serving so the platform cannot recycle it
$ aws autoscaling detach-instances --instance-ids i-0ab12cd34ef567890 \
--auto-scaling-group-name web-asg --should-decrement-desired-capacity
output
{
"Activities": [
{
"ActivityId": "9c3e0f4a-1b2c-4d5e-8f90-a1b2c3d4e5f6",
"AutoScalingGroupName": "web-asg",
"Description": "Detaching EC2 instance: i-0ab12cd34ef567890",
"Cause": "At 2026-07-14T09:41:52Z instance i-0ab12cd34ef567890 was detached in response to a user request, shrinking the capacity from 4 to 3.",
"StartTime": "2026-07-14T09:41:52.001000+00:00",
"StatusCode": "InProgress",
"Progress": 50,
"Details": "{\"Subnet ID\":\"subnet-0abc1234\",\"Availability Zone\":\"us-east-1a\"}"
}
]
}
terminal
# re-tag so the pre-staged deny-all firewall rule captures the VM
$ gcloud compute instances add-tags web-01 --zone=us-central1-a --tags=quarantine
output
Updated [https://www.googleapis.com/compute/v1/projects/prod-app/zones/us-central1-a/instances/web-01].
terminal
# bind a deny-all NSG to the instance's network card
$ az network nic update -g rg-prod -n web-01-nic --network-security-group nsg-quarantine
output
{
"name": "web-01-nic",
"networkSecurityGroup": {
"id": "/subscriptions/xxxx/resourceGroups/rg-prod/providers/Microsoft.Network/networkSecurityGroups/nsg-quarantine",
"resourceGroup": "rg-prod"
},
"provisioningState": "Succeeded",
"resourceGroup": "rg-prod"
}
Deny-all can lock out your own responders
A quarantine group that truly blocks everything also cuts the path your forensic tools ride in on: AWS Systems Manager Session Manager (SSM, which gives you a shell on the box without opening SSH, the standard remote-login service), Google Cloud OS Login or Identity-Aware Proxy (IAP), Azure Bastion (Azure's managed gateway onto the box). Clamp the box to zero and you can no longer reach it to grab memory. So capture volatile RAM first, or carve one narrow outbound rule to your evidence bucket before you seal the instance. Otherwise you end up with a preserved disk and a lost memory image, and the decrypted secrets and live command-and-control config only ever existed in that memory.

Freeze the disks and the memory

With the box sealed, copy the evidence before you touch anything destructive. A snapshot is a photocopy of a disk at one instant, taken through the control plane. It is crash-consistent, which means it captures the disk exactly as it sat at that moment, the way a disk looks after a power cut. That is fine for forensics, where you are reading the bytes, not booting a live database off them. Snapshot the boot volume and every data volume. Tag each one to the case number so your chain of custody (the paper trail proving nobody altered the evidence) holds up. Then copy the snapshot into a separate forensics account or project that the compromised principal has no route into. Evidence still sitting inside the blast radius is not evidence: an attacker holding the workload's role could delete or edit it. Snapshots cost real money and take minutes to hours at scale, so keep them scripted. A runbook that makes you stop and look up flag syntax mid-incident is not a runbook.

terminal
$ aws ec2 create-snapshot --volume-id vol-0abc123def4567890 --description "IR-8891 evidence" \
--tag-specifications 'ResourceType=snapshot,Tags=[{Key=case,Value=IR-8891}]'
output
{
"SnapshotId": "snap-0f1e2d3c4b5a6d7e8",
"VolumeId": "vol-0abc123def4567890",
"State": "pending",
"StartTime": "2026-07-14T09:42:11.000000+00:00",
"Progress": "",
"OwnerId": "123456789012",
"Description": "IR-8891 evidence",
"VolumeSize": 30,
"StorageTier": "standard",
"Encrypted": true,
"Tags": [
{
"Key": "case",
"Value": "IR-8891"
}
]
}
terminal
$ gcloud compute disks snapshot web-01 --zone=us-central1-a \
--snapshot-names=ir-8891-evidence --storage-location=us
output
Creating snapshot(s) ir-8891-evidence...done.
terminal
# snapshot created straight into a dedicated forensics resource group
$ az snapshot create -g rg-forensics -n ir-8891-evidence \
--source /subscriptions/xxxx/resourceGroups/rg-prod/providers/Microsoft.Compute/disks/web-01-osdisk
output
{
"creationData": {
"createOption": "Copy",
"sourceResourceId": "/subscriptions/xxxx/resourceGroups/rg-prod/providers/Microsoft.Compute/disks/web-01-osdisk"
},
"diskSizeGb": 30,
"diskState": "Unattached",
"location": "eastus",
"name": "ir-8891-evidence",
"provisioningState": "Succeeded",
"resourceGroup": "rg-forensics"
}

The control plane cannot photograph RAM. A snapshot copies disks, and the memory of a running machine sits outside its reach. To grab it you run an agent inside the guest operating system, something like AVML (Acquire Volatile Memory for Linux) or LiME (Linux Memory Extractor), and have it write the memory image straight to your evidence bucket. That image holds the live network connections, unencrypted payloads, and injected code that termination would have destroyed, so it goes first, through the narrow rule you left open before the network went dark.

The credential-compromise runbook

Most cloud incidents are not malware humming on a box. They are a leaked credential: an access key committed to a public code repository, a token lifted from the instance metadata endpoint (a special internal address, 169.254.169.254, where a machine fetches its own short-lived credentials), a service-account key file left on a laptop that walked out of a cafe. The runbook has four beats: revoke, reconstruct, hunt, restore.

The part that keeps burning teams is that credentials do not all die the same way. A long-lived key, an IAM (Identity and Access Management, the cloud's system for who-can-do-what) user's access key or a Google service-account key, you switch off directly and it is finished. A temporary session is a different animal. AWS Security Token Service (STS) credentials from an AssumeRole call, a federated login token, a Microsoft Entra ID (Microsoft's cloud identity service, once called Azure Active Directory) access token: none of these can be pulled back one at a time, and they stay valid until their own timer runs out. An STS session can be handed out to run for up to twelve hours. Deactivating the key that assumed the role does nothing to the sessions already minted from it. To kill live AWS sessions you deny by the clock: attach a policy to the role that rejects every action carrying a token issued before your cutoff, using the aws:TokenIssueTime condition (the stamp AWS puts on every temporary credential recording when it was born). That is exactly what the console's 'Revoke active sessions' button does behind the glass. On Google Cloud you disable the service account and its keys. In Entra you revoke the account's refresh tokens (the long-lived tokens it uses to mint fresh short-lived ones) and switch the account off; the short access tokens already handed out expire on their own, usually within an hour, though Entra's default lets them run 60 to 90 minutes. A compromised app identity (a service principal, Entra's name for a workload's own login) is the awkward case: its tokens cannot be revoked one by one either, so you strip its client secrets and certificates so it cannot mint new ones, then disable it.

revoke-older-sessions.json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"DateLessThan": { "aws:TokenIssueTime": "2026-07-14T09:40:00Z" }
}
}
]
}
terminal
# 1) stop the long-lived key minting new sessions (silent on success)
$ aws iam update-access-key --user-name app --access-key-id AKIAIOSFODNN7EXAMPLE --status Inactive
# 2) kill sessions already issued from it (silent on success)
$ aws iam put-role-policy --role-name app-role --policy-name AWSRevokeOlderSessions \
--policy-document file://revoke-older-sessions.json
# 3) verify the deny actually landed
$ aws iam get-role-policy --role-name app-role --policy-name AWSRevokeOlderSessions \
--query 'PolicyDocument.Statement[0].Condition'
output
{
"DateLessThan": {
"aws:TokenIssueTime": "2026-07-14T09:40:00Z"
}
}
terminal
# disable the leaked key first, then the service account itself
$ gcloud iam service-accounts keys disable a1b2c3d4e5f60718293a4b5c6d7e8f9012345678 \
$ gcloud iam service-accounts disable [email protected]
output
Disabled key [a1b2c3d4e5f60718293a4b5c6d7e8f9012345678] for service account [[email protected]].
Disabled service account [[email protected]].
terminal
# revoke every refresh token for the account, then switch it off
$ az rest --method POST \
--url "https://graph.microsoft.com/v1.0/users/[email protected]/revokeSignInSessions"
$ az ad user update --id [email protected] --account-enabled false
output
{
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#Edm.Boolean",
"value": true
}

Reconstruct what the credential touched

Stopping the bleeding is not the same as knowing what happened. For that you open the audit trail, the cloud's own ledger of every API call: who did what, when, and from which address. On AWS that ledger is CloudTrail, and CloudTrail Lake lets you query it in plain SQL. On Google Cloud it is Cloud Audit Logs. On Azure it is the Activity Log, paired with Entra sign-in logs for the identity side. Scrolling a JSON event log by hand during an incident is hopeless; a query lets you ask a precise question and get a precise list. Filter by the compromised principal and the incident window, then read off which APIs ran, from which source IPs, in which regions. A single finding from GuardDuty, Security Command Center, or Microsoft Defender is a place to start looking, not the whole story. The audit trail is the ground truth.

terminal
$ aws cloudtrail start-query --query-statement \
"SELECT eventTime, eventName, sourceIPAddress, awsRegion
FROM a1b2c3d4-5678-90ab-cdef-1234567890ab
WHERE userIdentity.accessKeyId = 'AKIAIOSFODNN7EXAMPLE'
AND eventTime > '2026-07-14 08:00:00'
ORDER BY eventTime"
output
{
"QueryId": "8e4f0c1a-2b3c-4d5e-6f70-1a2b3c4d5e6f"
}
terminal
$ aws cloudtrail get-query-results --query-id 8e4f0c1a-2b3c-4d5e-6f70-1a2b3c4d5e6f \
--query 'QueryResultRows[*]' --output text
output
2026-07-14T08:12:03Z CreateAccessKey 203.0.113.7 us-east-1
2026-07-14T08:12:44Z PutUserPolicy 203.0.113.7 us-east-1
2026-07-14T08:15:10Z RunInstances 203.0.113.7 ap-south-1
terminal
$ gcloud logging read \
'protoPayload.authenticationInfo.principalEmail="[email protected]"
AND protoPayload.methodName:("CreateServiceAccountKey" OR "SetIamPolicy")' \
--freshness=1d \
--format="table(timestamp, protoPayload.methodName, protoPayload.requestMetadata.callerIp)"
output
TIMESTAMP METHOD_NAME CALLER_IP
2026-07-14T08:19:22Z google.iam.admin.v1.CreateServiceAccountKey 203.0.113.7
2026-07-14T08:20:05Z SetIamPolicy 203.0.113.7
terminal
$ az monitor activity-log list --caller [email protected] \
--start-time 2026-07-14T08:00:00Z \
--query "[].{time:eventTimestamp, op:operationName.localizedValue, ip:httpRequest.clientIpAddress}" \
-o table
output
Time Op Ip
-------------------- -------------------------------- -----------
2026-07-14T08:22:31Z Create or Update Virtual Machine 203.0.113.7
2026-07-14T08:23:09Z Create role assignment 203.0.113.7

That timeline answers three questions in order. What did the attacker read or copy out (the exfiltration question). What did they change (new policies, new resources, deleted logs). And the one that decides whether the incident is actually over: did they set up a way back in. The first two you can read off the calls in front of you. The third you have to go looking for, because the handful of calls that build persistence hide inside a day of ordinary admin noise.

So hunt persistence on purpose, because a competent attacker plants a second door before you find the first. The tells rhyme across all three clouds: new users or access keys, a role's trust policy quietly rewritten to trust an attacker's account, fresh service-account keys, new Entra app registrations or federated credentials, resources launched in regions you never use (RunInstances in ap-south-1 from a shop that only runs in us-east-1 is the classic mining tell), logging switched off or redirected, and new grants at the organization level: a service control policy here, an IAM binding on a folder or org node there, a management-group role assignment. Every foothold you miss is a re-entry an hour later.

Restore, or burn it down

Only after every foothold is pulled and everything the credential could reach is rotated do you restore. Rebuild clean, then reconnect. Do not un-isolate the original box and call it recovered; that box is evidence now, not a server. And there is a harder call hiding in here. If the attacker held admin over the account, or the audit logs themselves were switched off or edited, you cannot forensically trust a word that account says. The honest move is to stop trusting it and rebuild in a fresh account or project, treating the old one as contaminated ground. The same misconfigurations you chase at the tail of an incident, public keys, over-broad trust policies, logging that drifted off, are the ones a Cloud Security Posture Management tool (CSPM, software that scans your cloud for risky settings around the clock) flags every single day, which is where the next lesson picks up.

The same three moves, one spelling per cloud
AWS
Isolate
quarantine SG + detach from ASG/ELB
Snapshot
ec2 create-snapshot, tagged to case
Kill live sessions
deny by aws:TokenIssueTime
Google Cloud
Isolate
re-tag to a deny-all firewall rule
Snapshot
compute disks snapshot
Kill live sessions
disable service account + its keys
Azure
Isolate
bind a lock-down NSG to the NIC
Snapshot
az snapshot create into forensics RG
Kill live sessions
revokeSignInSessions + disable the account
Isolate keeps memory alive and the network dead. Snapshot freezes the disk into a forensics account. Killing live sessions is where the three clouds differ most, so know each one cold before the incident.
Quick check
01You find a compromised EC2 instance still running the attacker's code. Which move preserves the most evidence?
Incorrect — Termination discards volatile RAM and usually the root volume, destroying the record of what was taken.
Incorrect — A reboot wipes the memory holding decrypted secrets and in-RAM malware, and may not remove disk persistence anyway.
Correct — the network dies, memory stays alive for capture, and a detached instance will not be recycled out from under you.
Incorrect — That keeps the blast radius live; you contain first, and containment must not wait on a quiet snapshot.
02Your app assumed 'app-role' 40 minutes ago, and an attacker is still calling APIs with those temporary credentials. You set the app's long-lived access key to Inactive, but the malicious calls keep coming. What actually stops them?
Incorrect — STS session credentials are tied to the role, not the user, so deleting the user leaves the live session working.
Correct — this is what 'Revoke active sessions' does, invalidating every token minted before that moment.
Incorrect — An STS session can last up to twelve hours; that is not containment, it is hoping.
Incorrect — Key rotation has nothing to do with the validity of an already-issued session token.
03Your CloudTrail Lake query for the leaked key returns, in order: CreateAccessKey, then PutUserPolicy, then RunInstances in ap-south-1 (a region you never use). You have already set the leaked key to Inactive. What is the right read and next move?
Incorrect — That ignores CreateAccessKey and PutUserPolicy, which are persistence, so the attacker walks straight back in.
Incorrect — RunInstances in an unused region from a stolen key is a classic mining signal, not a billing quirk.
Incorrect — The new key is an independent credential and stays fully active until you find and disable it yourself.
Correct — the attacker built a second way in, so hunting and rotating comes before any restore.

Try this

Work through “Restore, or burn it down” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.

Takeaway

The trap worth remembering here: deny-all can lock out your own responders. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.

Related