Skip to content

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:

  1. Operator pre-requisites (Plan 115 keypair must be live).
  2. Customer-facing setup commands (we hand the customer the script; they paste it into their Cloud Shell).
  3. Persisting the connection in Secruna (cp-api row carries audience + SA email; no KV secret).
  4. Verification + first discovery sweep.
  5. 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-configuration
  • https://api.secruna.com/oidc/jwks
  • The cp-api deployment has OIDC_ISSUER_PRIVATE_KEY_PEM populated 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} in scopes_granted so 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:

  1. Mints an RS256 JWT with aud = <wif_audience> and sub = secruna-discovery-{tenant_id}.
  2. POSTs to https://sts.googleapis.com/v1/token with the RFC 8693 token-exchange grant; receives a federated bearer token.
  3. POSTs to https://iamcredentials.googleapis.com/.../{sa_email}:generateAccessToken with Authorization: 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_runs row for the connection completes with status='completed' and artifacts_count > 0.
  • GCP audit log on the customer side shows iamcredentials.googleapis.com/GenerateAccessToken calls from the federated principal.
  • No KV access (oidc-issuer-private-key-pem aside) — confirm via Azure KV access logs that google-oauth-client-secret was 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 GCPOAuth 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.completed rows carry the connection id; the WIF subtype is observable via the connections.subtype column.
  • Structured logs: gcp_wif.exchange_for_federated_token / gcp_wif.impersonate_service_account emit RuntimeError with the Google-side error description on failure.
  • Google audit: the customer sees iam.googleapis.com/sts.token.exchange + iamcredentials.googleapis.com/GenerateAccessToken calls on their side, with the per-tenant subject (secruna-discovery-{tenant_id}) so they can attribute each discovery sweep to one Secruna tenant.