A GCP compromise walkthrough (fictional case study)
A fictional Google Cloud incident investigated end to end: leaked CI key, Owner for a Gmail account, startup-script backdoor, public bucket, deleted log sink.
TL;DR. This is a fictional incident: the company, people, project, keys and IP addresses are invented (IPs come from the documentation ranges of RFC 5737). It is the dataset behind Try a sample on the tool page. A CI service account key created for a local test leaks; six days later it is used from a VPS, which enumerates, grants roles/owner to a Gmail account, plants a startup script, downloads 140 objects, makes the bucket public and deletes the sink feeding the SIEM, all in 55 minutes. Every step below is a real finding the analyzer produces on that sample.
The point of a walkthrough is to show the reasoning, not the tool. Each step names the log field that carries the evidence, so you can reproduce it on your own data with Logs Explorer if you prefer.
The setting
"Northwind Analytics" (fictional) runs a project nw-analytics-prod with:
- a CI service account,
ci-deployer@…, used by Terraform; - an ETL VM,
etl-worker-1inus-central1-a, running asetl-runner@…and reading daily exports; - a bucket
nw-analytics-exportsholding customer extracts; - a sink
nw-audit-to-siemexporting audit logs to Pub/Sub for the SIEM; - Data Access logs enabled, and VPC Flow Logs on the default subnet.
The export is what you get by copying a sink bucket: 26 hourly JSON-lines shards under cloudaudit.googleapis.com/… and compute.googleapis.com/vpc_flows/…, plus one Logs Explorer download. The analyzer reads 215 audit entries and 65 flow records, and sets aside 3 GKE entries for Kubernetes Forensics.
The verdict
Compromised, with 16 findings: 3 critical, 8 high, 5 medium. The first reason is the correlation "attack chain: stolen access, then takeover" covering 02:03 to 02:57 UTC on 14 September.
A verdict is a claim; the rest of this article checks it.
September 8: the key is born
09:14:22 alex.martin@… google.iam.admin.v1.CreateServiceAccountKey from 198.51.100.24 (gcloud, macOS)
sa_key_created (medium). Alex creates a JSON key for ci-deployer to test Terraform locally. Minutes later the key is used from the same office IP with a Terraform/1.9.5 user agent: serviceAccountKeyName ends with …/keys/6f1c2d9e…. That fires sa_key_used_external (medium): the key is used from outside Google Cloud. On its own, that is ordinary. It becomes relevant because the office IP is now the key's first known IP.
The following days are routine and produce no findings: the ETL service account reads four objects every morning from the VM's internal IP, Priya lists instances, patches a firewall rule to the IAP range, grants roles/viewer to a colleague; Google live-migrates the VM (a System Event entry by system@google.com). This baseline is what makes the next day stand out. Export a baseline whenever you can.
September 14, 02:03: the key is used from somewhere else
02:03:11 ci-deployer@… GetIamPolicy from 203.0.113.66 (gcloud, Linux) key 6f1c2d9e…
sa_key_new_ip (high, initial access, T1078.004). Same key, new public IP, new client: gcloud on Linux instead of Terraform on macOS, at 2 a.m. The key has been copied. The leaked key article covers how to separate this from CI noise.
02:04 to 02:08: enumeration
24 calls in four minutes, all status.code: 7 (PERMISSION_DENIED): accessing the prod-db-password secret, reading the organization's IAM policy and org policies, the billing account, Security Command Center notification configs, service accounts in another project. permission_denied_burst (medium, discovery). This is the attacker learning what the key can do, and discovering that it cannot do much at organization level but has setIamPolicy on the project.
02:11: Owner for a Gmail account
02:11:07 ci-deployer@… SetIamPolicy ADD roles/owner user:nw.ops.backup@gmail.com
Three findings on one entry: owner_to_consumer (critical), owner_editor_granted (high), consumer_account_granted (high). The name was chosen to look like an internal backup account; the domain gives it away. Seven minutes later that Gmail account lists instances from the same VPS IP, in a browser: the attacker has switched to an identity that survives the key being disabled. See SetIamPolicy abuse.
02:24 to 02:26: backdoor on the VM
02:24:40 nw.ops.backup@gmail.com v1.compute.instances.setMetadata +startup-script etl-worker-1
02:25:10 nw.ops.backup@gmail.com v1.compute.projects.setCommonInstanceMetadata +ssh-keys -enable-oslogin
02:26:02 nw.ops.backup@gmail.com v1.compute.instances.reset etl-worker-1
startup_script_changed (high), ssh_key_added (medium), oslogin_changed (high). A startup script runs as root at boot; the reset makes it run now. Project-wide SSH key plus OS Login removed gives shell access to every VM. The audit log names the keys, not the script: a real investigation would snapshot the disk and read the metadata. See Compute Engine abuse.
02:27 onwards: the network confirms it
VPC Flow Logs show an inbound SSH session from 203.0.113.66 to the VM on port 22 at 02:27, then 36 outbound HTTPS flows from etl-worker-1 to the same IP until 02:54, about 1.9 GiB in total, destination geolocated in the Netherlands.
flow_large_egress (medium) and flow_ioc_match (high): the IP that used the leaked key is also the peer of the backdoored VM. This is the link between the control plane and what the VM actually did. See VPC Flow Logs analysis.
02:33 to 02:40: the bucket
140 storage.objects.get on nw-analytics-exports, objects named customers/2026/q3/customer-extract-0000.csv.gz onwards, in six and a half minutes, by the Gmail account: bulk_object_reads (high). Then:
02:40:15 nw.ops.backup@gmail.com storage.setIamPermissions ADD roles/storage.objectViewer allUsers
public_access_granted (critical). From 02:40 the bucket is public; reads by anonymous users would not appear in audit logs. The report must state an exposure window, not only the 140 proven reads. See GCS and BigQuery exfiltration.
02:57: covering tracks
02:57:42 nw.ops.backup@gmail.com google.logging.v2.ConfigServiceV2.DeleteSink nw-audit-to-siem
sink_deleted (high). From here, the SIEM receives nothing. The deletion itself sits in the _Required bucket, as does every Admin Activity entry before and after. See defense evasion.
Pivots that tie it together
- IP 203.0.113.66: 173 audit entries under two principals (the CI service account, then the Gmail account), plus the flow records. One click in the Entities tab filters the events table to it.
- Key 6f1c2d9e…: created by Alex, used from two IPs, the second one hostile.
- Principal nw.ops.backup@gmail.com: 147 entries, all from the VPS, first seen 02:18.
The checklist the tool generates
In order: declare an incident and preserve the logs; block the IP; remove the IAM binding and restrict member domains; make the bucket private and assess data exposure; disable the key and rotate what the CI service account could read; snapshot, inspect and rebuild the VM; re-enforce OS Login; restore the sink; forbid key creation; review the enumeration; remove SSH keys. The containment items come first because the attacker still holds two identities.
What this case teaches
- The leak was not the attack; the attack started six days later. Key creation events deserve review on their own.
- The attacker changed identity within eight minutes. Pivot on IPs, not only principals.
- Data Access logs and flow logs turned "maybe" into "140 files and 1.9 GiB". Without them, the limitations article applies.
To reproduce, open the analyzer, press Try a sample, and follow the step-by-step guide.
Further reading
- Google Cloud incident response: a compromised project.
- Respond to compromised Google Cloud credentials — Google Cloud documentation.
- MITRE ATT&CK IaaS matrix.