Scan GCP Projects with Workload Identity Federation
Use Workload Identity Federation to let a service account in one GCP project scan resources in other GCP projects without exporting keys.
Use Workload Identity Federation (WIF) to let a service account in one GCP project scan resources in other GCP projects — without exporting keys or creating service accounts in the target projects.
This guide uses direct resource access (no service account impersonation). You create a Workload Identity Pool in each target project that trusts a specific service account from your scanner project. At runtime, the scanner VM exchanges its Google-issued identity token for a federated access token that carries the granted roles in the target project.
You can also configure this approach using service account impersonation with WIF if your security policy requires it.
Example setup in this guide:
| Role | Project ID | Description |
|---|---|---|
| Scanner (source) | scanner-project | Runs the VM with cnspec installed |
| Target A | target-project-a | First project to be scanned |
| Target B | target-project-b | Second project to be scanned |
You repeat steps 1 through 5 for each target project. You configure the scanner project only once.
Prerequisites
- IAM permissions: You need
roles/iam.workloadIdentityPoolAdminon each target project andiam.serviceAccounts.geton the scanner project's service account. - APIs enabled on each target project:
iam.googleapis.comandcloudresourcemanager.googleapis.com - A GCE VM in the scanner project with a dedicated service account attached (not the default compute service account)
- cnspec installed on the VM
Enable the required APIs on each target project:
gcloud services enable iam.googleapis.com \
cloudresourcemanager.googleapis.com \
--project="target-project-a"
gcloud services enable iam.googleapis.com \
cloudresourcemanager.googleapis.com \
--project="target-project-b"Step 1: Gather scanner service account information
From any machine where you are authenticated as a project admin, look up the scanner service account's unique ID. This numeric ID is used in the trust condition instead of the email address because it is immutable — it survives service account deletion and recreation with the same name.
SCANNER_SA_EMAIL="scanner-sa@scanner-project.iam.gserviceaccount.com"
SCANNER_SA_ID=$(gcloud iam service-accounts describe "$SCANNER_SA_EMAIL" \
--format='value(uniqueId)')
echo "Scanner SA unique ID: $SCANNER_SA_ID"Step 2: Configure each target project
Repeat the remaining steps for each target project. The examples below show both target projects.
Set variables
LOCATION="global"Target A:
TARGET_PROJECT="target-project-a"
IDENTITY_POOL_ID="scanner-pool"
PROVIDER_ID="scanner-provider"
TARGET_PROJECT_NUMBER=$(gcloud projects describe "$TARGET_PROJECT" \
--format='value(projectNumber)')Target B:
TARGET_PROJECT="target-project-b"
IDENTITY_POOL_ID="scanner-pool"
PROVIDER_ID="scanner-provider"
TARGET_PROJECT_NUMBER=$(gcloud projects describe "$TARGET_PROJECT" \
--format='value(projectNumber)')Pool and provider IDs only need to be unique within a project. Using the same names across target projects is fine and simplifies management.
Step 3: Create the Workload Identity Pool and OIDC provider
Create the Workload Identity Pool
gcloud iam workload-identity-pools create "$IDENTITY_POOL_ID" \
--project="$TARGET_PROJECT" \
--location="$LOCATION" \
--display-name="Scanner Pool"Create the OIDC provider
The provider trusts Google-issued identity tokens and restricts authentication to the scanner service account by its unique ID.
gcloud iam workload-identity-pools providers create-oidc "$PROVIDER_ID" \
--project="$TARGET_PROJECT" \
--location="$LOCATION" \
--workload-identity-pool="$IDENTITY_POOL_ID" \
--display-name="Scanner GCP Provider" \
--issuer-uri="https://accounts.google.com" \
--attribute-mapping='google.subject=assertion.sub,attribute.sa_email=assertion.email' \
--attribute-condition="assertion.sub == \"${SCANNER_SA_ID}\""Key parameters:
issuer-uri: Set tohttps://accounts.google.combecause the identity token comes from Google's metadata server.attribute-mapping:google.subject=assertion.submaps the service account's unique ID as the WIF principal identifier.attribute.sa_email=assertion.emailis optional but useful for audit logs.attribute-condition: Restricts the provider so that only the scanner service account can authenticate. Without this, any Google-issued token could federate into the pool.
Step 4: Grant IAM roles to the WIF principal
Bind roles directly to the federated principal. No service account is created or impersonated in the target project.
MEMBER="principal://iam.googleapis.com/projects/$TARGET_PROJECT_NUMBER/locations/$LOCATION/workloadIdentityPools/$IDENTITY_POOL_ID/subject/$SCANNER_SA_ID"
gcloud projects add-iam-policy-binding "$TARGET_PROJECT" \
--member="$MEMBER" \
--role="roles/viewer"
gcloud projects add-iam-policy-binding "$TARGET_PROJECT" \
--member="$MEMBER" \
--role="roles/browser"Use principal:// (not principalSet://) when binding to a specific /subject/.
principalSet:// is for matching groups of identities (for example, by attribute or wildcard).
Two roles are required:
roles/viewerprovides the read access that cnspec needs for a project scan.roles/browsergrantsresourcemanager.projects.getandresourcemanager.projects.list, which the federated principal needs to resolve project metadata. Without this role, WIF tokens receiveACCESS_TOKEN_SCOPE_INSUFFICIENTerrors when calling the Cloud Resource Manager API.
You can use more granular roles if your security policy requires least-privilege scoping.
Step 5: Generate the credential configuration file
Each target project needs its own credential configuration file. This file tells the client library to fetch an identity token from the GCE metadata server and exchange it via the Security Token Service (STS) for a federated access token.
POOL_RESOURCE="projects/$TARGET_PROJECT_NUMBER/locations/$LOCATION/workloadIdentityPools/$IDENTITY_POOL_ID/providers/$PROVIDER_ID"
METADATA_AUDIENCE="//iam.googleapis.com/${POOL_RESOURCE}"
METADATA_URL="http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=${METADATA_AUDIENCE}"
cat > "wif-credential-${TARGET_PROJECT}.json" <<EOF
{
"universe_domain": "googleapis.com",
"type": "external_account",
"audience": "${METADATA_AUDIENCE}",
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
"token_url": "https://sts.googleapis.com/v1/token",
"credential_source": {
"url": "${METADATA_URL}",
"headers": {
"Metadata-Flavor": "Google"
},
"format": {
"type": "text"
}
},
"token_info_url": "https://sts.googleapis.com/v1/introspect"
}
EOFThe credential configuration file contains no secrets. It only describes how to obtain a token. Token exchange only succeeds from a GCE VM running with the correct service account attached.
Run the scans
Copy the generated credential files to the scanner VM. Before scanning, verify the VM is using the correct service account:
curl -s -H "Metadata-Flavor: Google" \
http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/email
# Expected: scanner-sa@scanner-project.iam.gserviceaccount.comScan each target project by pointing GOOGLE_APPLICATION_CREDENTIALS to the corresponding credential file:
GOOGLE_APPLICATION_CREDENTIALS="wif-credential-target-project-a.json" \
cnspec scan gcp project target-project-a
GOOGLE_APPLICATION_CREDENTIALS="wif-credential-target-project-b.json" \
cnspec scan gcp project target-project-bSecurity considerations
- No exported keys. Authentication relies on the GCE metadata server. No long-lived credentials are created, stored, or rotated.
- Scoped trust. The
attribute-conditionensures that only the specific scanner service account can federate into each target project's pool. Removing or recreating the service account with the same email does not grant access — the unique ID must match. - Least privilege. Each target project independently controls which IAM roles are granted to the federated principal. Revoking access is a single IAM policy binding removal.
- Audit trail. Federated access appears in Cloud Audit Logs under the WIF principal, making it straightforward to trace scanning activity.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
INVALID_ARGUMENT: Invalid principalSet member | Used principalSet:// with /subject/ | Change to principal:// |
PERMISSION_DENIED on scan | Missing IAM binding or wrong role | Verify the binding with gcloud projects get-iam-policy |
Unable to detect a valid identity token | VM is not using the correct service account | Check the metadata endpoint to confirm the SA email |
The audience in the identity token is invalid | Credential file has the wrong audience string | Regenerate the credential file with the correct project number and pool/provider IDs |
ACCESS_TOKEN_SCOPE_INSUFFICIENT on GetProject | WIF principal missing resourcemanager.projects.get | Grant roles/browser to the WIF principal on the target project |
Request had insufficient authentication scopes | VM missing cloud-platform scope | Recreate the VM with --scopes=cloud-platform or use a VM that has it |