
GCP penetration testing lives and dies on one feature that AWS and Azure handle differently: service accounts can impersonate each other. An identity that holds Token Creator over a more privileged account can mint a token as that account with a single flag and never needs a key, a password, or an exploit. Get the binding graph wrong by one role and a build script escalates to project Owner.
So this guide is organized around the GCP IAM model rather than a generic recon-to-report sequence. It opens with impersonation because that is the path that lands most often, then works through long-lived keys, the metadata SSRF and its scope gotcha, and the org policies that shut the whole class down. You will see the actual gcloud output at each step and a sample findings table. Written authorization from the project owner is assumed.
Google requires no notification or approval to test resources in your own GCP projects, but you must comply with the Google Cloud Acceptable Use Policy and Terms of Service. You test what you own: Compute Engine instances, Cloud Functions, Cloud Storage buckets, GKE clusters, and your IAM configuration. You do not test Google's underlying infrastructure or other customers.
Prohibited activity follows the familiar pattern: no denial-of-service or anything that disrupts Google's services or other tenants, no testing of resources you do not own. Unlike AWS, there is no permitted-services list and no simulated-events form; the constraint is behavioral, not a category whitelist. The practical guardrails are simple: keep activity inside authorized projects, never touch another customer's resources, and avoid traffic volumes that could degrade a shared Google service. Confirm your identity and project context first, because in GCP your reach is whatever this identity can impersonate:
$ gcloud auth list --format="value(account)"
build-runner@acme-prod.iam.gserviceaccount.com <- you are a service account, not a user
$ gcloud config get-value project
acme-prodBecause GCP testing is so identity-driven, it has more in common with reviewing an access model than running a network sweep, and it pairs with our overview of what to expect from a cloud penetration test.
The most reliable GCP escalation is direct impersonation, no exploit required. If your identity holds iam.serviceAccounts.getAccessToken (the roles/iam.serviceAccountTokenCreator role) over a more privileged service account, you mint a token as that account. First list the accounts in the project, then test which you can act as:
$ gcloud iam service-accounts list --format="value(email)"
build-runner@acme-prod.iam.gserviceaccount.com
terraform-admin@acme-prod.iam.gserviceaccount.com <- likely privileged, try impersonating it
$ gcloud auth print-access-token \
--impersonate-service-account=terraform-admin@acme-prod.iam.gserviceaccount.com
ya29.c.b0AX... <- success: we now hold terraform-admin's tokenConfirm the escalation end to end by running a privileged action under the borrowed identity. Binding yourself Owner with the impersonated account is the unambiguous proof:
$ gcloud projects add-iam-policy-binding acme-prod \
--member="user:attacker@example.com" --role="roles/owner" \
--impersonate-service-account=terraform-admin@acme-prod.iam.gserviceaccount.com
Updated IAM policy for project [acme-prod]. <- you are now OwnerThe defensive fix is surgical: grant Token Creator only where a workflow truly needs it, scope it to the specific target service account rather than the project, and prefer workload identity federation over standing impersonation grants. The other escalation families (setIamPolicy to bind yourself a role, iam.serviceAccountKeys.create to mint a key, deploy permissions plus actAs) follow the same map-the-graph discipline; Rhino Security Labs documented around two dozen of them.
Service-account keys are long-lived JSON credential files, and unlike short-lived metadata tokens they do not expire on their own. When a key leaks into a public repo, a container image, a CI variable, or instance metadata, whoever finds it authenticates as that account and uses every permission it holds until an administrator manually disables or deletes the key. During a test, hunt keys everywhere: source control, build artifacts, environment variables, and the metadata of compromised instances. Once you have one, activate it and read exactly what it can do:
$ gcloud auth activate-service-account --key-file=leaked-key.json
Activated service account credentials for: [ci-deploy@acme-prod.iam.gserviceaccount.com]
$ gcloud projects get-iam-policy acme-prod \
--flatten="bindings[].members" \
--filter="bindings.members:ci-deploy@acme-prod.iam.gserviceaccount.com" \
--format="value(bindings.role)"
roles/editor <- a leaked key with project-wide EditorThat single line, roles/editor, is the difference between a contained leak and a project takeover. The defensive position is to avoid user-managed keys entirely in favor of workload identity and short-lived tokens, then alert on any key creation. The leaked-credential problem is universal across cloud, which we also cover in the Azure penetration testing guide.
The GCP metadata server returns OAuth access tokens for the instance's attached service account, making it a prime SSRF target. From a compromised instance or an SSRF, request the token endpoint with the required Metadata-Flavor: Google header, and crucially also read the scopes:
$ curl -s -H "Metadata-Flavor: Google" \
"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token"
{"access_token":"ya29.c.b0A...","expires_in":3592,"token_type":"Bearer"}
$ curl -s -H "Metadata-Flavor: Google" \
"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/scopes"
https://www.googleapis.com/auth/cloud-platform <- full-access scope: the token can do anything the SA canThat mandatory header is the first defense; legacy /v1beta1/ endpoints did not require it but are deprecated, and the v1 endpoint rejects header-less requests. The second, GCP-specific limiter is OAuth scopes. An instance launched with restricted scopes hands you a token that cannot do much even if the service account has broad IAM roles. The cloud-platform scope above is the dangerous case: it lifts the cap entirely. Always check both the scopes and the underlying roles before you rate the finding, because the combination, not either one alone, determines impact. The mechanics echo the metadata attacks in our AWS penetration testing guide.
On a recent assessment of a fintech's GCP estate, the chain that landed was unglamorous. A build VM ran with the default Compute Engine service account, which still carried the broad Editor role, and the instance was launched with the cloud-platform scope. One SSRF in a webhook handler yielded a token that could do almost anything in the project. Before that, an unauthenticated bucket sweep had already given us the lay of the land:
$ python3 gcpbucketbrute.py -k acme -u
[+] acme-prod-backups: EXISTS - Anonymous user has [storage.objects.list] <- public listing
[+] acme-terraform-state: EXISTS - No access
$ gsutil ls gs://acme-prod-backups
gs://acme-prod-backups/db-dump-2026-05.sql.gzThe findings table below is that report excerpt. Each fix is an org-level control, not a one-off. Strip Editor from default service accounts and launch instances with the narrowest OAuth scopes. Grant Token Creator surgically. Enable Cloud Audit Logs across services. And kill the long-lived-key class entirely with an organization policy:
gcloud resource-manager org-policies enable-enforce \
iam.disableServiceAccountKeyCreation \
--organization 123456789012
# Also: constraints/iam.automaticIamGrantsForDefaultServiceAccounts (deny)One GCP-specific edge case worth knowing: a custom role can look harmless by name yet bundle a single dangerous permission like iam.serviceAccounts.getAccessToken, so never rate a role by its display name. Expand it with gcloud iam roles describe and read the actual permission list, because that is where the impersonation edge usually hides. The same applies to folder- and organization-level bindings, which inherit downward and are easy to miss when you only inspect the project policy. For tooling, run posture first with ScoutSuite (scout gcp --user-account) and Prowler to surface public buckets, over-permissive bindings, and default-SA Editor, then use the Rhino Security GCP IAM privilege-escalation scripts and the impersonation checks above for the chains scanners cannot see. Posture scanners flag the public bucket and the default-SA Editor binding but miss live impersonation chains and never replay your SSRF. Running this on a cadence rather than once a year is the case for continuous, agentic pentesting, and findings feed straight into a cloud security posture checklist.