GCP Workload Identity Federation — operator runbook¶
Plan 116 — kills GOOGLE_OAUTH_CLIENT_SECRET from Key Vault for the
GCP discovery connector in favour of customer-side Workload Identity
Federation (WIF). cp-api mints a JWT against our OIDC issuer (Plan
115), trades it for a federated token at Google STS, and impersonates
a customer-configured service account. No shared secret on our side;
the customer revokes by deleting the WIF binding in their own GCP
console.
This runbook covers:
- Operator pre-requisites (Plan 115 keypair must be live).
- Customer-facing setup commands (we hand the customer the script; they paste it into their Cloud Shell).
- Persisting the connection in Secruna (cp-api row carries audience + SA email; no KV secret).
- Verification + first discovery sweep.
- Rollback path if the customer's WIF binding breaks.
Pre-requisites¶
- Plan 115 OIDC issuer is provisioned and live. Both endpoints must return 200:
https://api.secruna.com/oidc/.well-known/openid-configurationhttps://api.secruna.com/oidc/jwks- The cp-api deployment has
OIDC_ISSUER_PRIVATE_KEY_PEMpopulated via the Plan 115 setup catalog. The discovery-worker reads the same PEM from env (OIDC_ISSUER_PRIVATE_KEY_PEM+OIDC_ISSUER_URL). - The customer holds GCP IAM Workload Identity Pool Admin on the target project for the duration of the setup. No standing privilege needed afterwards.
Step 1 — Hand the customer the gcloud setup script¶
The customer-facing onboarding doc
(docs/customer/gcp-wif-onboarding.md)
has the same script wrapped in user-friendly framing. The condensed
shape:
# 1. Identify your project.
PROJECT_ID=$(gcloud config get-value project)
PROJECT_NUMBER=$(gcloud projects describe "$PROJECT_ID" \
--format="value(projectNumber)")
# 2. Workload identity pool + OIDC provider that trusts Secruna.
gcloud iam workload-identity-pools create secruna-discovery \
--location=global \
--display-name="Secruna AI Discovery"
gcloud iam workload-identity-pools providers create-oidc secruna-issuer \
--workload-identity-pool=secruna-discovery \
--location=global \
--issuer-uri="https://api.secruna.com/oidc" \
--attribute-mapping="google.subject=assertion.sub"
# 3. Read-only service account.
gcloud iam service-accounts create secruna-reader \
--display-name="Secruna read-only discovery"
# 4. Grant the federated identity permission to impersonate the SA.
# The {tenant-id} placeholder is the Secruna tenant id; we provide
# it on the connection-add screen.
gcloud iam service-accounts add-iam-policy-binding \
"secruna-reader@${PROJECT_ID}.iam.gserviceaccount.com" \
--role="roles/iam.workloadIdentityUser" \
--member="principal://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/secruna-discovery/subjects/secruna-discovery-{tenant-id}"
# 5. Grant the SA the read roles discovery needs.
for role in roles/aiplatform.viewer roles/notebooks.viewer roles/browser; do
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="serviceAccount:secruna-reader@${PROJECT_ID}.iam.gserviceaccount.com" \
--role="$role"
done
# 6. Echo the values they need to paste into the Secruna dialog.
echo "Audience: //iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/secruna-discovery/providers/secruna-issuer"
echo "SA email: secruna-reader@${PROJECT_ID}.iam.gserviceaccount.com"
Sample IAM policy YAML (informational)¶
For customers using Terraform / Pulumi rather than the gcloud script:
# secruna_wif.yaml — paste into your IaC of choice.
google_iam_workload_identity_pool:
workload_identity_pool_id: secruna-discovery
display_name: "Secruna AI Discovery"
location: global
google_iam_workload_identity_pool_provider:
workload_identity_pool_id: secruna-discovery
workload_identity_pool_provider_id: secruna-issuer
attribute_mapping:
google.subject: "assertion.sub"
oidc:
issuer_uri: "https://api.secruna.com/oidc"
google_service_account:
account_id: secruna-reader
display_name: "Secruna read-only discovery"
google_service_account_iam_binding:
service_account_id: secruna-reader@${project_id}.iam.gserviceaccount.com
role: "roles/iam.workloadIdentityUser"
members:
- "principal://iam.googleapis.com/projects/${project_number}/locations/global/workloadIdentityPools/secruna-discovery/subjects/secruna-discovery-${tenant_id}"
google_project_iam_member:
for role in [roles/aiplatform.viewer, roles/notebooks.viewer, roles/browser]:
project: ${project_id}
role: ${role}
member: "serviceAccount:secruna-reader@${project_id}.iam.gserviceaccount.com"
Step 2 — Customer pastes audience + SA email in the dashboard¶
The customer opens Connections → Connect GCP in the Secruna dashboard. The dialog defaults to the Workload Identity Federation tab. They paste:
- WIF audience URI — full resource path. The frontend +
cp-api both validate against
^//iam\.googleapis\.com/projects/\d+/locations/global/workloadIdentityPools/[^/]+/providers/[^/]+$. - Service account email — must end with
.iam.gserviceaccount.com. - Project IDs — one per line. Persisted as
project:{id}inscopes_grantedso the orchestrator's resolver works the same way it does for the legacy refresh-token path.
The POST /connections/gcp/wif/connect endpoint persists a fresh
connections row with:
provider = 'gcp'subtype = 'wif'wif_audience = <pasted URI>wif_service_account_email = <pasted email>secret_ref = ''— no KV secret created, because there is no Secruna-side secret to hold.
Step 3 — Trigger a discovery run + verify¶
Click Run discovery on the new connection. cp-api enqueues a
run_discovery arq job. The worker's _build_gcp factory sees
subtype='wif', calls get_gcp_wif_access_token which:
- Mints an RS256 JWT with
aud = <wif_audience>andsub = secruna-discovery-{tenant_id}. - POSTs to
https://sts.googleapis.com/v1/tokenwith the RFC 8693 token-exchange grant; receives a federated bearer token. - POSTs to
https://iamcredentials.googleapis.com/.../{sa_email}:generateAccessTokenwithAuthorization: Bearer <federated>; receives the final SA access token.
The resulting token is injected directly into GCPConnector (its
access_token constructor arg short-circuits the refresh-token
exchange). The orchestrator then iterates Vertex AI / Notebooks /
Resource Manager APIs as usual.
Verification checklist:
discovery_runsrow for the connection completes withstatus='completed'andartifacts_count > 0.- GCP audit log on the customer side shows
iamcredentials.googleapis.com/GenerateAccessTokencalls from the federated principal. - No KV access (
oidc-issuer-private-key-pemaside) — confirm via Azure KV access logs thatgoogle-oauth-client-secretwas not read during the run.
Step 4 — Operator sanity-check commands¶
Reusable shell snippets for L1 support when a customer reports "discovery returns 0 artifacts":
# 1. Are the OIDC endpoints reachable + serving the same JWKS the
# worker's keypair derives?
curl -fsS https://api.secruna.com/oidc/.well-known/openid-configuration | jq .
curl -fsS https://api.secruna.com/oidc/jwks | jq .
# 2. Verify Google can fetch our JWKS without auth.
curl -fsS https://api.secruna.com/oidc/jwks -A "Google-OIDC"
# 3. Read the customer's WIF provider config (requires the customer to
# grant you Workload Identity Pool Viewer for this).
gcloud iam workload-identity-pools providers describe secruna-issuer \
--workload-identity-pool=secruna-discovery \
--location=global
# 4. Confirm the iam.workloadIdentityUser binding on the SA — this is
# the most common misconfig. The ``member:`` must match the JWT
# subject we send.
gcloud iam service-accounts get-iam-policy \
"secruna-reader@${PROJECT_ID}.iam.gserviceaccount.com"
Step 5 — Rollback / disable¶
If the WIF binding breaks (customer rotated the SA, deleted the pool,
etc.) the discovery worker will surface a clean RuntimeError from
exchange_for_federated_token or impersonate_service_account. The
discovery run lands in status='failed' with the upstream Google
error in error_jsonb.
The customer can either:
- Re-run the gcloud script to re-create the binding, then click Run discovery again. No Secruna-side action needed.
- Fall back to the legacy refresh-token flow. Delete the WIF
connection and click Connect GCP → OAuth refresh token
(legacy) tab. The legacy path stays available until 80%+ of GCP
connections are on WIF (per the migration plan in
docs/roadmap/2026-05-13-plan-116-gcp-workload-identity-federation.md).
There is no Secruna-side rollback to perform — we hold no per-customer secret to clean up.
Audit + observability¶
- Audit log:
discovery.run.started+discovery.run.completedrows carry the connection id; the WIF subtype is observable via theconnections.subtypecolumn. - Structured logs:
gcp_wif.exchange_for_federated_token/gcp_wif.impersonate_service_accountemitRuntimeErrorwith the Google-side error description on failure. - Google audit: the customer sees
iam.googleapis.com/sts.token.exchange+iamcredentials.googleapis.com/GenerateAccessTokencalls on their side, with the per-tenant subject (secruna-discovery-{tenant_id}) so they can attribute each discovery sweep to one Secruna tenant.