GCP security assessment and enumeration methodology

GCP Security Assessment and Enumeration

GCP security assessment methodology covering project enumeration, service account discovery, storage bucket exposure, and metadata API exploitation.

Oct 6, 2026
2 min read

Introduction

GCP's security model revolves around projects, service accounts, and IAM bindings rather than the policy-document approach used by AWS. Every API call in GCP is governed by Cloud IAM, and the blast radius of a compromised identity depends entirely on what roles are bound to it across which projects. Service accounts are the primary target — they can be bound to GCE instances, impersonated by other accounts, and can have JSON key files exported and used from anywhere on the internet.

The GCP metadata service at http://metadata.google.internal/computeMetadata/v1/ is the fastest pivot point from a compromised GCE instance. It exposes the bound service account, its access token, and full project details with a single HTTP request. Unlike AWS IMDSv2, GCP's metadata endpoint does not require session-oriented token exchange — the Metadata-Flavor: Google header is the only protection, making SSRF exploitation trivial.

Initial access context dictates the starting methodology. From a compromised GCE instance you pull tokens directly from the metadata API. With a stolen service account JSON key, you authenticate with gcloud auth activate-service-account. With an OAuth token obtained via phishing or token theft, you configure it directly via gcloud config set auth/access_token_file. Each path lands in the same enumeration workflow once you have working credentials.

Authorization Required

GCP assessments must have explicit written authorization covering the specific project IDs and services in scope. Cloud audit logs record every API call with caller IP and identity. Unauthorized enumeration against GCP projects violates Google's Terms of Service and triggers automated abuse detection.

Impact

  • Service account with roles/owner or roles/editor on a project grants full control over all resources including billing, compute, and storage
  • Publicly readable Cloud Storage buckets expose data with no authentication and leave no trace in access logs unless Data Access audit logging is explicitly enabled
  • Service account JSON keys persist indefinitely unless rotated — a leaked key file is a permanent backdoor until revoked
  • Workload Identity Federation misconfigurations allow external identities (GitHub Actions, AWS roles) to impersonate GCP service accounts
  • Organization-level IAM policy mistakes propagate to every project under the org
  • Cloud Functions and Cloud Run services running with overprivileged service accounts allow privilege escalation through code execution
  • Default compute service account has roles/editor on the project in older GCP environments — GCE instances inherit this by default

Technical Details

GCP's IAM hierarchy flows from Organization → Folder → Project → Resource. Roles bound at a higher level inherit downward. The resourcemanager.projects.list permission lets you enumerate all projects visible to your identity, which is the first step in mapping the attack surface.

Service accounts exist in a specific project but can be granted roles in other projects. The IAM policy for a project (getIamPolicy) returns every principal with bindings, including external service accounts from other projects. Cross-project service account usage is a common lateral movement path.

Cloud Storage ACLs operate at two levels: the bucket ACL (legacy) and IAM policies. Uniform bucket-level access (UBL) disables per-object ACLs and centralizes control in IAM. When UBL is disabled, individual objects can be publicly accessible even if the bucket policy appears restricted.

The metadata endpoint returns short-lived access tokens (1-hour expiry) tied to the instance's bound service account. The token scope is determined by the access scopes configured at instance creation — cloud-platform scope gives the token access to all GCP APIs limited only by the service account's IAM roles.

Identify Initial Access Context

Determine the access vector before starting enumeration.

Check current identity
# With gcloud configured
gcloud auth list
gcloud config list

# With OAuth token (e.g., from SSRF or token theft)
curl -H "Authorization: Bearer $TOKEN" \
  https://www.googleapis.com/oauth2/v1/tokeninfo

# Activate service account from JSON key
gcloud auth activate-service-account \
  --key-file=/path/to/service-account-key.json

# Set project context
gcloud config set project <PROJECT_ID>

Pull Credentials from GCE Metadata Service

From a compromised GCE instance, the metadata API gives you the bound service account identity and a live access token.

Enumerate metadata service
BASE="http://metadata.google.internal/computeMetadata/v1"
HDR='-H "Metadata-Flavor: Google"'

# Project info
curl -s $BASE/project/project-id $HDR
curl -s $BASE/project/numeric-project-id $HDR

# Instance info
curl -s $BASE/instance/name $HDR
curl -s $BASE/instance/zone $HDR
curl -s $BASE/instance/service-accounts/ $HDR

# Service account details
curl -s $BASE/instance/service-accounts/default/email $HDR
curl -s $BASE/instance/service-accounts/default/scopes $HDR

# Pull access token (expires in ~1 hour)
curl -s $BASE/instance/service-accounts/default/token $HDR
# Returns: {"access_token":"ya29.xxx","expires_in":3599,"token_type":"Bearer"}

# SSH keys and custom metadata
curl -s $BASE/instance/attributes/ $HDR
curl -s $BASE/instance/attributes/ssh-keys $HDR

Export the token and use it directly for API calls:

Use metadata token for API calls
TOKEN=$(curl -s http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token \
  -H "Metadata-Flavor: Google" | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")

curl -H "Authorization: Bearer $TOKEN" \
  "https://cloudresourcemanager.googleapis.com/v1/projects"

Enumerate Projects and Organization

Map the full scope of accessible resources.

Project enumeration
# List all accessible projects
gcloud projects list --format="table(projectId,name,projectNumber,lifecycleState)"

# Organization discovery
gcloud organizations list

# Folders (requires resourcemanager.folders.list)
gcloud resource-manager folders list --organization=<ORG_ID>

# Project ancestors (full hierarchy)
gcloud projects get-ancestors <PROJECT_ID>

IAM Policy Enumeration

IAM policies reveal every identity with access and what they can do.

Dump IAM policies
# Project-level IAM policy
gcloud projects get-iam-policy <PROJECT_ID> \
  --format=json > project-iam-policy.json

# Organization-level policy (high value if accessible)
gcloud organizations get-iam-policy <ORG_ID> \
  --format=json > org-iam-policy.json

# List all service accounts in project
gcloud iam service-accounts list \
  --project=<PROJECT_ID> \
  --format="table(email,displayName,disabled)"

# Service account IAM policy (who can act as this SA)
gcloud iam service-accounts get-iam-policy <SA_EMAIL> \
  --format=json

# Check for SA keys (key material exported externally)
gcloud iam service-accounts keys list \
  --iam-account=<SA_EMAIL> \
  --filter="keyType=USER_MANAGED"

Parse the policy for overprivileged bindings:

Find overprivileged bindings
# Find all bindings with primitive roles (owner/editor/viewer)
gcloud projects get-iam-policy <PROJECT_ID> \
  --format=json | \
  python3 -c "
import sys, json
p = json.load(sys.stdin)
for b in p.get('bindings', []):
    role = b['role']
    if 'owner' in role or 'editor' in role or 'admin' in role:
        print(f'{role}:')
        for m in b['members']:
            print(f'  {m}')
"

# Find allUsers / allAuthenticatedUsers bindings (public access)
gcloud projects get-iam-policy <PROJECT_ID> \
  --format=json | \
  grep -E "allUsers|allAuthenticatedUsers"

Enumerate Compute Resources

Compute instance enumeration
# All instances across all zones
gcloud compute instances list --format="table(name,zone,status,serviceAccounts.email)"

# Instance details including startup scripts (may contain secrets)
gcloud compute instances describe <INSTANCE_NAME> \
  --zone=<ZONE> \
  --format=json

# Firewall rules — look for overly permissive ingress
gcloud compute firewall-rules list \
  --format="table(name,direction,priority,sourceRanges,targetTags,allowed)"

# VPC networks
gcloud compute networks list

# Check for instances with public IPs and broad SSH access
gcloud compute firewall-rules list \
  --filter="direction=INGRESS AND allowed.ports:22 AND sourceRanges:0.0.0.0/0"

# Cloud Run services (often publicly accessible)
gcloud run services list --platform=managed

# Cloud Functions
gcloud functions list
gcloud functions describe <FUNCTION_NAME> --format=json

Cloud Storage Bucket Enumeration and ACL Audit

Storage bucket enumeration
# List all buckets in project
gsutil ls -p <PROJECT_ID>

# Alternative with gcloud
gcloud storage buckets list --project=<PROJECT_ID> \
  --format="table(name,location,storageClass,iamConfiguration.uniformBucketLevelAccess.enabled)"

# Check bucket ACL (legacy ACL, only relevant if UBL disabled)
gsutil acl get gs://<BUCKET_NAME>

# Check IAM policy on bucket
gsutil iam get gs://<BUCKET_NAME>

# Check if bucket allows public access
gsutil iam get gs://<BUCKET_NAME> | grep -E "allUsers|allAuthenticatedUsers"

# List bucket contents (if readable)
gsutil ls -la gs://<BUCKET_NAME>

# Recursive list (careful with large buckets)
gsutil ls -r gs://<BUCKET_NAME>/**

# Check object ACLs on specific file (when UBL is disabled)
gsutil acl get gs://<BUCKET_NAME>/<OBJECT_KEY>

# Download interesting files
gsutil cp gs://<BUCKET_NAME>/credentials.json ./
gsutil cp gs://<BUCKET_NAME>/terraform.tfstate ./

Check for public access across all project buckets:

Audit all buckets for public access
for bucket in $(gsutil ls -p <PROJECT_ID>); do
    echo "=== $bucket ==="
    gsutil iam get $bucket 2>/dev/null | grep -E "allUsers|allAuthenticatedUsers" \
      && echo "  [!] PUBLIC ACCESS FOUND" || echo "  [ok] Not public"
done

Enumerate Enabled Services and APIs

The enabled services list reveals the attack surface — BigQuery data, Kubernetes clusters, Secret Manager entries.

Service enumeration
# All enabled services
gcloud services list --enabled \
  --format="table(config.name,config.title)"

# Kubernetes clusters
gcloud container clusters list \
  --format="table(name,location,status,currentMasterVersion)"

# Cloud SQL instances
gcloud sql instances list

# Cloud Spanner
gcloud spanner instances list

# Secret Manager secrets (names only — need secretmanager.versions.access for values)
gcloud secrets list --format="table(name,createTime,replication)"

# Access a secret version (requires secretmanager.versions.access)
gcloud secrets versions access latest --secret=<SECRET_NAME>

# BigQuery datasets
bq ls --project_id=<PROJECT_ID>

# Pub/Sub topics and subscriptions
gcloud pubsub topics list
gcloud pubsub subscriptions list

Attack Tools

ScoutSuite automates multi-service configuration assessment and generates an HTML report with findings categorized by severity.

ScoutSuite GCP scan
# Install
pip install scoutsuite

# Scan with application default credentials
scout gcp --user-account

# Scan with specific project
scout gcp --user-account --project-id <PROJECT_ID>

# Scan with service account key
scout gcp --service-account /path/to/key.json

# Generate report to specific directory
scout gcp --user-account --report-dir ./scoutsuite-report

# Scan specific services only
scout gcp --user-account --services iam,gcs,compute,cloudsql

# Skip specific services
scout gcp --user-account --skip cloudfunctions,cloudbuild

ScoutSuite checks for: public storage buckets, overprivileged service accounts, weak firewall rules, unencrypted resources, audit logging disabled, MFA enforcement, and OS Login configuration.

Parse ScoutSuite JSON findings
# ScoutSuite also outputs raw JSON
python3 -c "
import json
with open('scoutsuite-results/scoutsuite_results_gcp-<project>.js') as f:
    # Strip the leading 'scoutsuite_results =' assignment
    data = json.loads(f.read().split('=',1)[1].strip().rstrip(';'))
    
services = data.get('services', {})
for svc, svc_data in services.items():
    findings = svc_data.get('findings', {})
    for finding_id, finding in findings.items():
        if finding.get('flagged_items', 0) > 0:
            print(f'[{svc}] {finding[\"description\"]}: {finding[\"flagged_items\"]} items')
"

enumerate-iam.py brute-forces which IAM permissions your current identity holds by attempting API calls across all GCP services. Useful for understanding the true capability of a compromised token.

enumerate-iam.py
# Clone and install
git clone https://github.com/aquasecurity/enumerate-iam-gcp
pip install -r requirements.txt

# Run against current gcloud credentials
python3 enumerate_iam.py \
  --project-id <PROJECT_ID>

# Run with explicit access token
python3 enumerate_iam.py \
  --project-id <PROJECT_ID> \
  --access-token ya29.xxxxx

# Run against specific service account
python3 enumerate_iam.py \
  --project-id <PROJECT_ID> \
  --service-account-key /path/to/key.json

# Output to file
python3 enumerate_iam.py \
  --project-id <PROJECT_ID> \
  --output-file permissions.json

The tool makes ~500+ API calls to test permissions. Expect this to generate significant audit log volume. Key permissions to look for:

PermissionImpact
iam.serviceAccounts.actAsImpersonate any SA in project
iam.serviceAccountKeys.createGenerate persistent SA keys
iam.roles.updateEscalate privileges via role modification
resourcemanager.projects.setIamPolicyGrant arbitrary IAM roles
compute.instances.setMetadataInject SSH keys or startup scripts
secretmanager.versions.accessRead secrets
storage.objects.getRead bucket contents

Native gcloud enumeration with specific flags useful during assessments.

gcloud assessment commands
# Identify the current authenticated identity
gcloud auth list
gcloud config get-value account
gcloud config get-value project

# Test specific permission (policy simulator — requires own permissions)
gcloud policy-intelligence simulate-access \
  --resource=//cloudresourcemanager.googleapis.com/projects/<PROJECT_ID> \
  --principal=serviceAccount:<SA_EMAIL> \
  --permission=compute.instances.list

# Check testable permissions for a resource
gcloud iam list-testable-permissions \
  //cloudresourcemanager.googleapis.com/projects/<PROJECT_ID> \
  --format="table(name,stage)"

# Find all custom roles defined in project
gcloud iam roles list --project=<PROJECT_ID>

# Inspect a custom role's permissions
gcloud iam roles describe <ROLE_ID> --project=<PROJECT_ID>

# Audit organization policies (constraints)
gcloud resource-manager org-policies list \
  --project=<PROJECT_ID>

# Check if domain restriction constraint is set (prevents external accounts)
gcloud resource-manager org-policies describe \
  constraints/iam.allowedPolicyMemberDomains \
  --project=<PROJECT_ID>

# Cloud Logging — check if data access logs are enabled
gcloud projects get-iam-policy <PROJECT_ID> \
  --format=json | python3 -c "
import sys, json
p = json.load(sys.stdin)
for ac in p.get('auditConfigs', []):
    print(ac['service'])
    for lc in ac.get('auditLogConfigs', []):
        print(f'  {lc[\"logType\"]}')
"

GCPBucketBrute enumerates GCS bucket names and checks permissions without authentication.

GCPBucketBrute
git clone https://github.com/RhinoSecurityLabs/GCPBucketBrute
pip install -r requirements.txt

# Enumerate using keyword wordlist
python3 gcpbucketbrute.py \
  -k company-name \
  -o results.txt

# With custom wordlist
python3 gcpbucketbrute.py \
  -k company-name \
  -w /usr/share/wordlists/bucket-names.txt \
  -o results.txt

# With authenticated session (finds buckets you may not have unauthenticated access to)
python3 gcpbucketbrute.py \
  -k company-name \
  --auth

# Checks performed per bucket:
# - Bucket exists (HTTP 200 vs 404/403)
# - Public listable (allUsers storage.buckets.list)
# - Public readable objects (allUsers storage.objects.get)
# - Authenticated read/write/admin (allAuthenticatedUsers)

Naming patterns to target:

company-name
company-name-backup
company-name-prod
company-name-dev
company-name-staging
company-name-logs
company-name-assets
company-name-media
company-name-tf-state
company-name-terraform

Detection

GCP Cloud Audit Logs are the primary detection data source. There are three log types: Admin Activity (always on, no charge), Data Access (disabled by default, must be explicitly enabled per service), and System Event (automatic).

Critical gap: If Data Access audit logging is not enabled, reads from Cloud Storage, Secret Manager, and other data services are invisible in logs. Attackers reading bucket contents or secrets leave no trace unless this is configured.

Cloud Audit Log sources
# Admin Activity logs — always enabled
logName="projects/<PROJECT>/logs/cloudaudit.googleapis.com%2Factivity"

# Data Access logs — must be enabled
logName="projects/<PROJECT>/logs/cloudaudit.googleapis.com%2Fdata_access"

# System events
logName="projects/<PROJECT>/logs/cloudaudit.googleapis.com%2Fsystem_event"

Key event types to alert on:

EventLog SourceIndicator
GetIamPolicy from unexpected IPData AccessIAM policy dumping
ListBuckets + ListObjects in sequenceData Access (GCS)Storage enumeration
serviceAccounts.keys.createAdmin ActivitySA key exfiltration
SetIamPolicy granting primitive rolesAdmin ActivityPrivilege escalation
instances.setMetadataAdmin ActivitySSH key injection
secrets.versions.accessData AccessSecret exfiltration
GetAccessToken from non-GCE sourceSystemToken theft/replay

SIEM detection queries (Log Analytics / Chronicle):

Detect SA key creation
-- Service account key creation — user-managed keys are persistent backdoors
SELECT
  timestamp,
  protoPayload.authenticationInfo.principalEmail AS actor,
  protoPayload.resourceName AS service_account,
  protoPayload.requestMetadata.callerIp AS source_ip
FROM
  `<PROJECT>.cloudaudit_googleapis_com_activity`
WHERE
  protoPayload.methodName = "google.iam.admin.v1.CreateServiceAccountKey"
  AND protoPayload.request.privateKeyType = "TYPE_GOOGLE_CREDENTIALS_FILE"
ORDER BY timestamp DESC
Detect public IAM bindings
-- Alert when allUsers or allAuthenticatedUsers added to any IAM policy
SELECT
  timestamp,
  protoPayload.authenticationInfo.principalEmail AS actor,
  protoPayload.resourceName AS resource,
  protoPayload.request AS request_detail
FROM
  `<PROJECT>.cloudaudit_googleapis_com_activity`
WHERE
  protoPayload.methodName IN ("SetIamPolicy", "google.iam.v1.IAMPolicy.SetIamPolicy")
  AND (
    TO_JSON_STRING(protoPayload.request) LIKE '%allUsers%'
    OR TO_JSON_STRING(protoPayload.request) LIKE '%allAuthenticatedUsers%'
  )
Detect metadata service queries from unexpected sources
-- Metadata queries from non-GCE IPs (potential SSRF exploitation)
SELECT
  timestamp,
  protoPayload.authenticationInfo.principalEmail,
  protoPayload.requestMetadata.callerIp,
  protoPayload.methodName
FROM
  `<PROJECT>.cloudaudit_googleapis_com_data_access`
WHERE
  protoPayload.requestMetadata.callerIp NOT LIKE "10.%"
  AND protoPayload.requestMetadata.callerIp NOT LIKE "172.16.%"
  AND protoPayload.requestMetadata.callerIp NOT LIKE "192.168.%"
  AND protoPayload.serviceName = "iamcredentials.googleapis.com"
  AND protoPayload.methodName = "GenerateAccessToken"

Behavioral indicators by attacker privilege level:

  • Viewer role only: High-volume Get* and List* calls across multiple services in short succession — characteristic of automated enumeration
  • Editor role: serviceAccounts.keys.create without corresponding deployment activity; instances.setMetadata with new SSH keys
  • Owner / setIamPolicy: Role binding changes adding external accounts or primitive roles

Remediation

IAM hardening:

  • Eliminate roles/owner and roles/editor bindings; replace with least-privilege custom roles or predefined roles scoped to specific resources
  • Enforce constraints/iam.allowedPolicyMemberDomains org policy to prevent external account bindings
  • Disable service account key creation org-wide with constraints/iam.disableServiceAccountKeyCreation unless operationally required
  • Enable constraints/iam.disableServiceAccountKeyUpload to prevent importing external keys
  • Audit iam.serviceAccounts.actAs bindings — this permission allows full impersonation of the target SA
  • Require Workload Identity Federation instead of exported JSON keys for non-GCP workloads

Compute hardening:

  • Do not use the default compute service account; create dedicated SAs with minimal permissions
  • Disable access scopes that grant broad API access (cloud-platform scope); specify only required scopes
  • Enable OS Login (enable-oslogin=true metadata) to use Cloud IAM for SSH instead of metadata-based keys
  • Enable Shielded VMs and Confidential Computing for sensitive workloads
  • Block project-wide SSH key inheritance at instance level for sensitive instances

Storage hardening:

  • Enable Uniform Bucket-Level Access (UBL) on all buckets — eliminates per-object ACL complexity
  • Enable Public Access Prevention at org or bucket level
  • Audit for allUsers and allAuthenticatedUsers IAM bindings regularly
  • Enable Data Access audit logging for Cloud Storage in all production projects
  • Use VPC Service Controls to restrict storage access to specific VPC networks

Audit logging:

  • Enable Data Access audit logs for: Cloud Storage, Secret Manager, BigQuery, IAM, Cloud KMS
  • Export logs to a separate project with restricted access to prevent tampering
  • Set log retention to minimum 400 days for compliance
  • Alert on SetIamPolicy, CreateServiceAccountKey, and AccessSecretVersion events

Secret management:

  • Migrate all secrets from environment variables, startup scripts, and instance metadata to Secret Manager
  • Rotate all service account keys; move to keyless auth where possible
  • Scan GCS buckets and instance metadata for embedded credentials as part of regular audits

References

MITRE ATT&CK Techniques

Tools Documentation

Next Steps

On this page