Google Cloud Platform (GCP)
Set up with an AI agent
Using Claude Code, Cursor, Codex CLI, or another coding agent? The subimage-setup:connect-gcp skill walks the agent through every choice on this page. Paste this into your agent:
Use curl to download, read and follow: https://skills.subimage.io/plugins/subimage-setup/skills/connect-gcp/SKILL.mdOr install the marketplace once with claude plugin marketplace add subimagesec/skills. Full instructions on Agent Skills.
Purpose
Inventories all projects, folders, and organization-level settings inside your Google Cloud organization.
Typical asset classes collected:
- Compute Engine instances, images, and disks
- Cloud SQL instances and Cloud Storage buckets
- IAM policies (users, service accounts, roles)
- Organization / folder hierarchy and Org Policies
- API keys used to authenticate to public Google APIs
Secret fields below accept either an AWS Secrets Manager ARN or a value pasted directly into SubImage's managed vault. See Secrets for details.
Required Fields
Choose one authentication method.
| Field | Secret? | Description |
|---|---|---|
gcp_wif_project_number |
No | Project number for the project that owns the Workload Identity Pool |
gcp_wif_pool_id |
No | Workload Identity Pool ID |
gcp_wif_provider_id |
No | Workload Identity Provider ID inside the pool |
gcp_service_account_key |
Yes | Service account JSON key. Use only if you cannot use Workload Identity Federation |
Required IAM roles (org level)
| Role | Purpose | Required |
|---|---|---|
roles/iam.securityReviewer |
Read IAM policies, relationships, and Workload Identity Federation pools/providers | Yes |
roles/compute.viewer |
List and get Compute Engine inventory, including instances, networking, SSL policies, and target proxies | Yes |
roles/resourcemanager.organizationViewer |
Discover the organization, projects, and folders | Yes |
roles/resourcemanager.folderViewer |
Enumerate folder hierarchy | Yes |
roles/cloudasset.viewer |
Sync effective IAM policy bindings across the hierarchy. Attack paths and IAM permission analysis depend on it | Yes |
roles/run.viewer |
Sync Cloud Run services, jobs, executions, and revisions. Revisions hold the image digest behind a service deployed by tag; without them, SubImage cannot link those services to their images, so it cannot scan them or build vulnerability action items | Yes |
roles/notebooks.viewer |
Sync Vertex AI Workbench (Notebooks API) resources | Optional |
Custom role with apikeys.keys.list |
Sync GCP API key inventory. Use this instead of roles/serviceusage.apiKeysViewer, which also grants apikeys.keys.getKeyString and can read every key's secret value. See API key inventory |
Optional |
roles/cloudsql.viewer |
List Cloud SQL instances, databases, and users | Optional |
roles/bigquery.dataViewer |
List BigQuery datasets and tables across projects | Optional |
roles/bigquery.connectionUser |
Access BigQuery connection resources | Optional |
roles/artifactregistry.reader |
Pull container images from Artifact Registry for vulnerability and SBOM scanning | Required if scanning GAR images |
Setup Steps
- Pick or create a host project that will own the Workload Identity Pool. Discovery and Cloud Asset Inventory API calls are charged to it.
- Recommended: configure Workload Identity Federation so SubImage can use short-lived credentials without a JSON key.
- Grant the six required roles at the org root to the SubImage WIF principal. Add optional roles for broader coverage. If you plan to scan container images stored in Google Artifact Registry, grant
roles/artifactregistry.readeron the projects or repositories that contain the images. If you grant a custom role instead ofroles/iam.securityReviewer, ensure it includesiam.workloadIdentityPools.listandiam.workloadIdentityPoolProviders.list(or grantroles/iam.workloadIdentityPoolVieweralongside it); without these, WIF pools and providers are silently skipped. - Enable the required APIs on the host project: see API enablement.
- If you cannot use WIF, create a service account in the host project, grant the same roles to that service account, generate a JSON key, and enter it in
gcp_service_account_key(or its AWS Secret ARN). The service account's project is the quota project for discovery and Cloud Asset calls, so the host project APIs must be enabled there. - Optionally configure:
gcp_requested_syncs: select a subset of GCP resource groups for faster, scoped syncs. Leave it empty to sync everything supported by this Cartography version.gcp_enable_cai_iam_fallback: leave this enabled if you want SubImage to backfill service accounts and custom roles from Cloud Asset Inventory when a target project's IAM API is disabled.
- Save the module, then open the Run Sync page to start a GCP sync immediately or wait for the next scheduled run.
Selective Sync
SubImage can scope GCP syncs to a subset of supported resource groups, similar to the AWS module. This is useful when you want shorter syncs or only care about a specific slice of GCP inventory.
Examples:
compute,storage,iamfor a core infra inventory passbigquery,bigquery_connectionfor data-platform coveragegke,policy_bindings,permission_relationshipsfor Kubernetes plus IAM relationship analysis
Caveats:
- If you leave
gcp_requested_syncsempty, SubImage syncs all supported GCP resource groups. policy_bindingsdepends on IAM role data, so keepingiamselected is recommended.permission_relationshipsdepends on bothiamandpolicy_bindings.bigquery_connectiondepends onbigquery.
API Enablement
SubImage uses two kinds of GCP APIs, and they are enabled in different places.
- The host project is the project that owns the Workload Identity Pool, or the service account if you use a JSON key.
- The quota project is the project Google charges an API call to, and the API must be enabled there. For resource APIs such as Compute Engine, it is always the project that contains the resource. For client-billed APIs such as Cloud Asset Inventory, Google takes it from the caller's credentials. SubImage does not set one explicitly, so it is the host project.
Host project APIs. Discovery, Workload Identity Federation, and Cloud Asset Inventory calls use the host project as their quota project. Enable these on the host project:
gcloud services enable cloudresourcemanager.googleapis.com --project=YOUR_HOST_PROJECT
gcloud services enable serviceusage.googleapis.com --project=YOUR_HOST_PROJECT
gcloud services enable iam.googleapis.com --project=YOUR_HOST_PROJECT
# Required when using Workload Identity Federation
gcloud services enable sts.googleapis.com --project=YOUR_HOST_PROJECT
# Required for IAM policy bindings, which attack paths depend on
gcloud services enable cloudasset.googleapis.com --project=YOUR_HOST_PROJECTResource APIs. For every other resource type, SubImage checks which APIs are enabled in each scanned project and syncs a resource type only where its API is enabled in that project. A project that runs a service already has that service's API enabled, so you do not need to enable anything else. Enabling a resource API on the host project covers only the host project's own resources.
If a resource type is missing for one project, enable its API in that project, for example gcloud services enable compute.googleapis.com --project=THAT_PROJECT. When a resource API is disabled in a project, SubImage skips that resource type for that project; the rest of the sync continues.
Cloud Asset Inventory (CAI)
SubImage uses the Cloud Asset Inventory API for enhanced IAM coverage:
- IAM Fallback: when the IAM API is disabled on a target project, SubImage falls back to CAI to retrieve service accounts and custom roles. Requires
cloudasset.googleapis.comenabled on the host project. - Policy Bindings: syncs effective IAM policies (including inherited ones from parent orgs and folders) for all resources. Requires
cloudasset.googleapis.comenabled on the host project androles/cloudasset.viewerat the organization level. - Disabling
gcp_enable_cai_iam_fallbackonly turns off the IAM fallback. To avoid CAI-backed policy bindings too, deselectpolicy_bindingsfromgcp_requested_syncs.
Policy bindings are what attack paths follow once an attacker reaches a GCP identity, and what marks Cloud Run services and Cloud Functions as publicly invokable. Without them, GCP attack paths stop at the entry point and never appear. When policy bindings cannot be synced for a project, the GCP sync finishes as Degraded and names how many projects are affected.
Workload Identity Federation
Workload Identity Federation lets the SubImage tenant AWS role exchange its AWS identity for short-lived Google credentials. This avoids storing a long-lived service account key.
When you view these docs inside your authenticated tenant, SubImage auto-fills your tenant AWS account ID and tenant ID in the snippets below:
| Value | Auto-filled value |
|---|---|
| SubImage tenant AWS account ID | |
| SubImage tenant ID | |
| SubImage AWS role name | |
| Suggested Workload Identity Pool ID | subimage-wip |
| Suggested AWS provider ID | subimage-aws-provider |
The role ARN format is:
arn:aws:iam::<ACCOUNT_ID>:role/<TENANT_ID>-subimage-readonlySubImage stores only the GCP project number, pool ID, and provider ID. It does not need a Google credential configuration file and does not impersonate a Google service account for WIF. Bind the GCP IAM roles directly to the WIF principal shown below; otherwise token exchange can succeed while GCP APIs still return PERMISSION_DENIED.
The principal member you grant in Google Cloud should look like this after replacing the host project number:
principalSet://iam.googleapis.com/projects/<HOST_PROJECT_NUMBER>/locations/global/workloadIdentityPools/subimage-wip/attribute.aws_role/arn:aws:sts::<ACCOUNT_ID>:assumed-role/<TENANT_ID>-subimage-readonlyOption A: Terraform
variable "subimage_org_id" { type = string }
variable "subimage_host_project" { type = string }
variable "subimage_tenant_account_id" {
type = string
default = "<ACCOUNT_ID>"
}
variable "subimage_tenant_id" {
type = string
default = "<TENANT_ID>"
}
data "google_project" "host" {
project_id = var.subimage_host_project
}
locals {
subimage_aws_role_name = "${var.subimage_tenant_id}-subimage-readonly"
subimage_assumed_role_arn = "arn:aws:sts::${var.subimage_tenant_account_id}:assumed-role/${local.subimage_aws_role_name}"
subimage_wif_member = "principalSet://iam.googleapis.com/projects/${data.google_project.host.number}/locations/global/workloadIdentityPools/${google_iam_workload_identity_pool.subimage.workload_identity_pool_id}/attribute.aws_role/${local.subimage_assumed_role_arn}"
}
resource "google_iam_workload_identity_pool" "subimage" {
project = var.subimage_host_project
workload_identity_pool_id = "subimage-wip"
display_name = "SubImage AWS"
}
resource "google_iam_workload_identity_pool_provider" "subimage_aws" {
project = var.subimage_host_project
workload_identity_pool_id = google_iam_workload_identity_pool.subimage.workload_identity_pool_id
workload_identity_pool_provider_id = "subimage-aws-provider"
display_name = "SubImage AWS"
aws {
account_id = var.subimage_tenant_account_id
}
attribute_mapping = {
"google.subject" = "assertion.arn"
"attribute.account" = "assertion.account"
"attribute.aws_role" = "assertion.arn.contains('assumed-role') ? assertion.arn.extract('{account_arn}assumed-role/') + 'assumed-role/' + assertion.arn.extract('assumed-role/{role_name}/') : assertion.arn"
}
attribute_condition = "assertion.arn.startsWith('${local.subimage_assumed_role_arn}/')"
}
locals {
required_roles = [
"roles/iam.securityReviewer",
"roles/compute.viewer",
"roles/resourcemanager.organizationViewer",
"roles/resourcemanager.folderViewer",
"roles/cloudasset.viewer",
"roles/run.viewer",
]
optional_roles = [
# Add if scanning GAR images across many projects. For least privilege,
# grant roles/artifactregistry.reader at the repository or project scope instead.
# "roles/artifactregistry.reader",
]
all_roles = concat(local.required_roles, local.optional_roles)
}
resource "google_organization_iam_member" "subimage" {
for_each = toset(local.all_roles)
org_id = var.subimage_org_id
role = each.key
member = local.subimage_wif_member
}
output "subimage_gcp_wif_project_number" {
value = data.google_project.host.number
}
output "subimage_gcp_wif_pool_id" {
value = google_iam_workload_identity_pool.subimage.workload_identity_pool_id
}
output "subimage_gcp_wif_provider_id" {
value = google_iam_workload_identity_pool_provider.subimage_aws.workload_identity_pool_provider_id
}Option B: gcloud
ORG_ID=<ORG_ID>
HOST_PROJECT=<HOST_PROJECT>
TENANT_ACCOUNT_ID=<ACCOUNT_ID>
TENANT_ID=<TENANT_ID>
POOL_ID=subimage-wip
PROVIDER_ID=subimage-aws-provider
SUBIMAGE_AWS_ROLE_NAME="${TENANT_ID}-subimage-readonly"
SUBIMAGE_ASSUMED_ROLE_ARN="arn:aws:sts::${TENANT_ACCOUNT_ID}:assumed-role/${SUBIMAGE_AWS_ROLE_NAME}"
HOST_PROJECT_NUMBER="$(gcloud projects describe "$HOST_PROJECT" --format='value(projectNumber)')"
SUBIMAGE_WIF_MEMBER="principalSet://iam.googleapis.com/projects/${HOST_PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL_ID}/attribute.aws_role/${SUBIMAGE_ASSUMED_ROLE_ARN}"
gcloud iam workload-identity-pools create "$POOL_ID" \
--project="$HOST_PROJECT" \
--location=global \
--display-name="SubImage AWS"
gcloud iam workload-identity-pools providers create-aws "$PROVIDER_ID" \
--project="$HOST_PROJECT" \
--location=global \
--workload-identity-pool="$POOL_ID" \
--account-id="$TENANT_ACCOUNT_ID" \
--attribute-mapping="google.subject=assertion.arn,attribute.account=assertion.account,attribute.aws_role=assertion.arn.contains('assumed-role') ? assertion.arn.extract('{account_arn}assumed-role/') + 'assumed-role/' + assertion.arn.extract('assumed-role/{role_name}/') : assertion.arn" \
--attribute-condition="assertion.arn.startsWith('${SUBIMAGE_ASSUMED_ROLE_ARN}/')"
for ROLE in \
roles/iam.securityReviewer \
roles/compute.viewer \
roles/resourcemanager.organizationViewer \
roles/resourcemanager.folderViewer \
roles/cloudasset.viewer \
roles/run.viewer; do
gcloud organizations add-iam-policy-binding "$ORG_ID" \
--member="$SUBIMAGE_WIF_MEMBER" \
--role="$ROLE"
done
# Required if SubImage should scan images in Google Artifact Registry.
# Prefer repository or project scope for least privilege when practical.
gcloud artifacts repositories add-iam-policy-binding "<REPOSITORY>" \
--project="<GAR_PROJECT>" \
--location="<LOCATION>" \
--member="$SUBIMAGE_WIF_MEMBER" \
--role="roles/artifactregistry.reader"Optional: API key inventory
To sync GCP API keys, create a custom role with only apikeys.keys.list and bind it at the same scope as the required roles. Creating an organization custom role needs roles/iam.organizationRoleAdmin. On the JSON-key path, use serviceAccount:<SERVICE_ACCOUNT_EMAIL> as the member.
resource "google_organization_iam_custom_role" "subimage_apikeys_list" {
org_id = var.subimage_org_id
role_id = "subimageApiKeysList"
title = "SubImage API key inventory"
permissions = ["apikeys.keys.list"]
}
resource "google_organization_iam_member" "subimage_apikeys_list" {
org_id = var.subimage_org_id
role = google_organization_iam_custom_role.subimage_apikeys_list.name
member = local.subimage_wif_member
}gcloud iam roles create subimageApiKeysList \
--organization="$ORG_ID" \
--title="SubImage API key inventory" \
--permissions=apikeys.keys.list
gcloud organizations add-iam-policy-binding "$ORG_ID" \
--member="$SUBIMAGE_WIF_MEMBER" \
--role="organizations/${ORG_ID}/roles/subimageApiKeysList"Then enter these values in Modules → GCP:
| SubImage field | Value |
|---|---|
gcp_wif_project_number |
$HOST_PROJECT_NUMBER |
gcp_wif_pool_id |
$POOL_ID |
gcp_wif_provider_id |
$PROVIDER_ID |