GKE Workload Identity
This implementation guide assumes you have an existing GKE cluster with Workload Identity enabled and want to install RunWhen Local with no long-lived service-account keys. RunWhen Local authenticates through Application Default Credentials (ADC), which on GKE means the pod’s bound Google Service Account (GSA) is used automatically.
This is the GCP equivalent of EKS Workload Identity and Azure Managed Identity.
1. Prerequisites
- Existing GKE cluster with Workload Identity enabled (cluster + a node pool using the GKE metadata server).
kubectl,gcloud, andhelmconfigured for that cluster and project.- Permissions to create IAM service accounts, IAM policy bindings, and Kubernetes resources in the cluster.
2. Namespace and Kubernetes Base
Create the namespace and the service account RunWhen Local will run as:
kubectl create namespace runwhen-localkubectl create serviceaccount runwhen-local -n runwhen-localThe Helm chart can also create the SA; if you create it yourself, keep the name/namespace below so the rest of the guide matches.
3. Create a Google Service Account (GSA)
Create one Google Service Account that RunWhen Local and the runner will use:
export PROJECT_ID=my-gcp-projectexport GSA_NAME=runwhen-local-discovery
gcloud iam service-accounts create "$GSA_NAME" \ --project="$PROJECT_ID" \ --description="Service Account for RunWhen Discovery" \ --display-name="RunWhen Discovery Service Account"Record the GSA email: $GSA_NAME@$PROJECT_ID.iam.gserviceaccount.com
4. Attach IAM Permissions to the GSA
Grant the discovery roles RunWhen Local needs. At minimum:
# Broad read access for cloud resource discoverygcloud projects add-iam-policy-binding "$PROJECT_ID" \ --member="serviceAccount:$GSA_NAME@$PROJECT_ID.iam.gserviceaccount.com" \ --role="roles/viewer"
# GKE cluster list/get (required for auto-discovery)gcloud projects add-iam-policy-binding "$PROJECT_ID" \ --member="serviceAccount:$GSA_NAME@$PROJECT_ID.iam.gserviceaccount.com" \ --role="roles/container.viewer"For least privilege, replace roles/viewer with the per-service viewer roles your generation rules actually touch (e.g. roles/compute.viewer, roles/storage.objectViewer, roles/pubsub.viewer, roles/iam.securityReviewer).
For the optional Cloud Asset Inventory accelerator (broader resource coverage), additionally:
gcloud services enable cloudasset.googleapis.com --project="$PROJECT_ID"gcloud projects add-iam-policy-binding "$PROJECT_ID" \ --member="serviceAccount:$GSA_NAME@$PROJECT_ID.iam.gserviceaccount.com" \ --role="roles/cloudasset.owner"CAI is not required — the typed baseline collectors run normally without it.
5. Bind the KSA to the GSA (Workload Identity)
Allow the Kubernetes Service Account to impersonate the GSA:
gcloud iam service-accounts add-iam-policy-binding \ "$GSA_NAME@$PROJECT_ID.iam.gserviceaccount.com" \ --role="roles/iam.workloadIdentityUser" \ --member="serviceAccount:$PROJECT_ID.svc.id.goog[runwhen-local/runwhen-local]" \ --project="$PROJECT_ID"Then annotate the KSA with the GSA email:
kubectl annotate serviceaccount runwhen-local -n runwhen-local \ iam.gke.io/gcp-service-account="$GSA_NAME@$PROJECT_ID.iam.gserviceaccount.com"The RunWhen Local pod must run as this KSA (serviceAccountName: runwhen-local).
6. Grant the GSA Access to the GKE Cluster
For Kubernetes discovery, the GSA needs to be able to call the GKE cluster API. The roles/container.viewer role (granted in step 4) covers container.clusters.list and container.clusters.get, which is what RunWhen Local uses to fetch cluster endpoints and CA certs at discovery time.
For the actual Kubernetes API calls (listing namespaces, pods, etc.), RunWhen Local mints a short-lived OAuth bearer token from the GSA and uses it as the kubeconfig user credential. Ensure the GSA (or the Google group it belongs to) has appropriate RBAC access in the cluster. At minimum, create a ClusterRoleBinding for read access:
kubectl create clusterrolebinding runwhen-local-viewer \ --clusterrole=view \ --user="$GSA_NAME@$PROJECT_ID.iam.gserviceaccount.com"7. workspaceInfo Configuration
RunWhen Local discovers the cluster via the GKE Container API and then uses the same credentials to read the cluster. Put this in a workspaceInfo.yaml that you’ll load as a ConfigMap (step 8).
With Workload Identity, no credential fields are needed — leaving saSecretName, serviceAccountKey, and applicationCredentialsFile empty means ADC (and therefore Workload Identity) is used:
workspaceName: "my-runwhen-workspace"workspaceOwnerEmail: "you@example.com"defaultLocation: location-01defaultLOD: detailed
gcpIndexerBackend: gcpapi # default; native SDK, no CloudQuery
cloudConfig: gcp: # No credential fields -> ADC -> Workload Identity projects: - my-gcp-project gkeClusters: autoDiscover: true discoveryConfig: projectId: my-gcp-project # explicit clusters are merged with discovered ones clusters: - name: prod-cluster location: us-east1 defaultNamespaceLOD: detailed namespaceLODs: kube-system: none
codeCollections: - repoURL: "https://github.com/runwhen-contrib/google-cloud-codecollection" branch: "main" - repoURL: "https://github.com/runwhen-contrib/rw-cli-codecollection" branch: "main"Adjust projects and gkeClusters to match your environment. For a single explicit cluster instead of auto-discovery, set autoDiscover: false and list the cluster under clusters with name and location.
Do not set
GOOGLE_APPLICATION_CREDENTIALSin the pod environment when using Workload Identity. That env var forces ADC to read a key file and short-circuits the metadata-server path.
8. Load workspaceInfo into the Cluster
Create a ConfigMap from the file you created in step 7:
kubectl create configmap workspaceinfo -n runwhen-local --from-file=workspaceInfo.yamlIf the ConfigMap already exists, replace it:
kubectl create configmap workspaceinfo -n runwhen-local --from-file=workspaceInfo.yaml \ --dry-run=client -o yaml | kubectl apply -f -9. Install RunWhen Local with Helm
Use the RunWhen Local Helm chart and point it at the existing namespace, service account, and workspaceInfo ConfigMap.
- Service account: use the same
runwhen-localSA inrunwhen-localnamespace so it gets the GSA credentials via Workload Identity. - Runner: set runner deployment and runner pods to use the same
runwhen-localservice account so they share the identity.
Example values (customize image, upload, and code collections as needed):
# values.yaml (excerpt)
workspaceBuilder: serviceAccount: create: false # we created it manually in step 2 name: runwhen-local serviceAccountRoles: clusterRoleView: enabled: true workspaceInfo: useExistingConfigMap: true existingConfigMapName: workspaceinfo configMap: create: false # With Workload Identity, cluster discovery and auth both flow through ADC. # Do NOT enable inClusterAuth — GKE discovery uses the GCP Container API via # the GSA token, not the pod's in-cluster SA token. discoveryKubeconfig: inClusterAuth: enabled: false
runner: enabled: true runEnvironment: deployment: serviceAccount: runwhen-local pod: serviceAccount: runwhen-local # ... rest of runner config (codeCollections, controlAddr, etc.)Install:
helm upgrade --install runwhen-local ./path-to-chart \ -n runwhen-local \ -f values.yaml10. Verify
-
GCP identity from a pod:
Terminal window kubectl run -it --rm debug --restart=Never -n runwhen-local \--serviceaccount=runwhen-local \--image=google/cloud-sdk:slim -- \gcloud auth listOr verify the metadata server is returning the GSA token:
Terminal window kubectl run -it --rm debug --restart=Never -n runwhen-local \--serviceaccount=runwhen-local \--image=google/cloud-sdk:slim -- \curl -s -H "Metadata-Flavor: Google" \"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/email"The identity should be the GSA you created (e.g.
runwhen-local-discovery@my-gcp-project.iam.gserviceaccount.com). -
GKE cluster access:
The same pod should be able to list nodes:
Terminal window kubectl run -it --rm debug --restart=Never -n runwhen-local \--serviceaccount=runwhen-local \--image=bitnami/kubectl:latest -- \get nodes -
RunWhen Local: Check discovery and runner pods; logs should show no GCP auth errors and successful GKE/Kubernetes discovery.
Runtime Authentication for Generated Tasks
Generated tasks (codebundles) re-authenticate using the same ADC path. The secret-provider templates resolve the gcp_adc auth type to:
For GCP cloud resources (CLI):
secretsProvided: - name: gcp_credentials workspaceKey: gcp:adc@cliFor GKE clusters (kubeconfig):
secretsProvided: - name: kubeconfig workspaceKey: gcp:adc@kubeconfig:prod-cluster/us-east1The gcp:adc@* workspaceKeys consume the pod’s ambient ADC (Workload Identity) at task-execution time — no secret injection needed, provided the runner pod uses the same annotated KSA. If your runner is a different pod than the discovery pod, repeat the KSA annotation + IAM binding for that runner namespace/service-account.
Summary Checklist
| Step | What you did |
|---|---|
| 1 | Confirmed existing GKE cluster with Workload Identity enabled |
| 2 | Created namespace runwhen-local and SA runwhen-local |
| 3 | Created Google Service Account for RunWhen discovery |
| 4 | Granted IAM roles (roles/viewer, roles/container.viewer) to the GSA |
| 5 | Bound KSA to GSA via IAM policy + KSA annotation |
| 6 | Granted GSA read access to the GKE cluster (ClusterRoleBinding) |
| 7 | Wrote workspaceInfo.yaml with no credential fields and gkeClusters config |
| 8 | Created ConfigMap workspaceinfo from workspaceInfo.yaml in runwhen-local |
| 9 | Installed Helm with SA runwhen-local, existing ConfigMap, and runner using same SA |
| 10 | Verified GSA identity and cluster access from a pod using runwhen-local SA |
Single namespace, single service account (runwhen-local), single Google Service Account, and one IAM binding give both the RunWhen Local controller and the runner cluster-reader access and GCP discovery using Workload Identity on your existing GKE cluster.
Extending Beyond the Default Viewer Role
The base setup grants roles/viewer (cloud-level read-only) and a view ClusterRoleBinding (Kubernetes read-only). Many real-world workflows need more than read access — restarting workloads, deleting pods for run ops, reconciling Flux resources, or reading custom resources that the built-in view ClusterRole doesn’t aggregate.
The recommended pattern keeps GCP IAM minimal (roles/viewer for cloud discovery only) and extends all Kubernetes-level access via in-cluster RBAC. This avoids pulling in broad GKE IAM group roles (container.developer → edit, container.admin → cluster-admin) which grant cluster-wide write or admin that a discovery workload should not have.
Why not GKE IAM group roles?
GKE maps predefined IAM roles to in-cluster groups:
| GCP IAM role | GKE group | In-cluster equivalent |
|---|---|---|
roles/container.viewer | gcp-viewers | view ClusterRole |
roles/container.developer | gcp-developers | edit ClusterRole |
roles/container.admin | gcp-admins | cluster-admin |
container.developer (edit) grants create/update/delete on all built-in resources across every namespace — far too broad for a discovery SA. container.admin is cluster-admin. Instead, grant roles/viewer in GCP IAM and layer narrowly-scoped in-cluster RBAC for exactly the write operations you need.
Example: Discovery SA with targeted run-ops
This example shows a RunWhen Local deployment that needs:
- Cluster-wide read — discover resources in all namespaces
- Targeted delete — reset a specific stateful workload (e.g. a database pod and its PVC) to clear corrupted state
- Flux reconciliation — patch a single Flux
Kustomizationto trigger a reconcile
All three are bound to the GSA email as a kind: User subject — not the Kubernetes ServiceAccount. With GKE Workload Identity, the Kubernetes API server sees the GSA email as the authenticated identity, so RBAC must reference the GSA (User), not the KSA (ServiceAccount).
Helm values — annotate the SAs
The Helm chart creates the workspace-builder and runner service accounts. Add the annotation so they federate to the GSA:
workspaceBuilder: serviceAccount: create: true annotations: iam.gke.io/gcp-service-account: runwhen-local-discovery@my-gcp-project.iam.gserviceaccount.com
runner: serviceAccount: create: true annotations: iam.gke.io/gcp-service-account: runwhen-local-discovery@my-gcp-project.iam.gserviceaccount.comBoth SAs federate to the same GSA. The GSA’s GCP IAM stays at roles/viewer only.
In-cluster RBAC — bind to the GSA identity (User)
Apply these manifests to the cluster (via Flux, kubectl, or your GitOps tool of choice). The subjects use kind: User with the GSA email — the identity the Kubernetes API server sees when a Workload Identity-federated pod makes a call:
# --- 1. Cluster-wide read (same as base setup) ---apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRoleBindingmetadata: name: runwhen-discovery-viewersubjects: - kind: User name: runwhen-local-discovery@my-gcp-project.iam.gserviceaccount.com apiGroup: rbac.authorization.k8s.ioroleRef: kind: ClusterRole name: view apiGroup: rbac.authorization.k8s.io---# --- 2. Targeted delete in a specific namespace ---# Allow resetting a specific stateful workload by deleting its pod and PVC.apiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata: name: runwhen-discovery-targeted-delete namespace: my-apprules: - apiGroups: [""] resources: ["persistentvolumeclaims"] resourceNames: ["data-app-db-0"] verbs: ["delete"] - apiGroups: [""] resources: ["pods"] resourceNames: ["app-db-0"] verbs: ["delete"]---apiVersion: rbac.authorization.k8s.io/v1kind: RoleBindingmetadata: name: runwhen-discovery-targeted-delete namespace: my-appsubjects: - kind: User name: runwhen-local-discovery@my-gcp-project.iam.gserviceaccount.com apiGroup: rbac.authorization.k8s.ioroleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: runwhen-discovery-targeted-delete---# --- 3. Flux Kustomization reconcile (single resource) ---apiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata: name: runwhen-discovery-flux-reconcile namespace: flux-systemrules: - apiGroups: ["kustomize.toolkit.fluxcd.io"] resources: ["kustomizations"] resourceNames: ["my-app-sync"] verbs: ["get", "patch", "update"]---apiVersion: rbac.authorization.k8s.io/v1kind: RoleBindingmetadata: name: runwhen-discovery-flux-reconcile namespace: flux-systemsubjects: - kind: User name: runwhen-local-discovery@my-gcp-project.iam.gserviceaccount.com apiGroup: rbac.authorization.k8s.ioroleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: runwhen-discovery-flux-reconcileKey design choices:
kind: Usersubjects — the GSA email is the identity GKE presents to the Kubernetes API. Binding tokind: ServiceAccount(the KSA) would have no effect; the API server authenticates the federated GSA, not the KSA.resourceNamesscoping on the Role — the delete permission only applies to the specific pod and PVC named, not all resources of that type in the namespace.- Namespace-scoped Roles — the Flux patch is limited to a single
KustomizationCR by name influx-system; no cluster-wide CRD write. - Single subject — the GSA email covers all pods federated to it (both
workspace-builderandrunner), so no need to list multiple SAs. - Cross-cluster portability — because RBAC binds to the GSA email (User) rather than a KSA, the same Role/RoleBinding works on any cluster where a pod federated to that GSA calls the API. No local KSA needs to exist in the target cluster.
Custom resource discovery (CRDs)
The built-in view ClusterRole covers standard Kubernetes resources but not custom resource instances (CRs). If your discovery needs to read CRs — Flux Kustomizations, GitRepositories, CrunchyData PostgresClusters, or RunWhen’s own SLX resources — you have two options:
Option A: Aggregated ClusterRole (recommended)
Create a ClusterRole with the rbac.authorization.k8s.io/aggregate-to-view: "true" label. Kubernetes automatically merges it into the built-in view ClusterRole, so every SA bound to view inherits the CR read permissions — no extra RoleBinding needed:
apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata: name: aggregate-runwhen-view labels: rbac.authorization.k8s.io/aggregate-to-view: "true"rules: # Flux CD - apiGroups: ["kustomize.toolkit.fluxcd.io"] resources: ["*"] verbs: ["get", "list", "watch"] - apiGroups: ["source.toolkit.fluxcd.io"] resources: ["*"] verbs: ["get", "list", "watch"] # CrunchyData Postgres operator - apiGroups: ["postgres-operator.crunchydata.com"] resources: ["*"] verbs: ["get", "list", "watch"] # Metrics - apiGroups: ["metrics.k8s.io"] resources: ["pods", "nodes"] verbs: ["get", "list"]This is the cleanest approach for discovery-only workloads — one ClusterRole, no per-SA bindings, automatically inherited by anything bound to view.
Option B: Explicit ClusterRoleBinding
If you prefer not to modify the view ClusterRole globally, create a standalone ClusterRole + ClusterRoleBinding bound to the GSA identity:
apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata: name: runwhen-crd-viewerrules: - apiGroups: ["kustomize.toolkit.fluxcd.io"] resources: ["*"] verbs: ["get", "list", "watch"] # ... add other apiGroups as needed---apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRoleBindingmetadata: name: runwhen-crd-viewersubjects: - kind: User name: runwhen-local-discovery@my-gcp-project.iam.gserviceaccount.com apiGroup: rbac.authorization.k8s.ioroleRef: kind: ClusterRole name: runwhen-crd-viewer apiGroup: rbac.authorization.k8s.ioUse Option B when the CRD read access should be scoped to only the RunWhen GSA and not every principal bound to view.
Cross-project discovery
When RunWhen Local runs in a GKE cluster in project B but needs to discover resources in project A (e.g. a shared services project), the GSA lives in project A and is impersonated by the cluster in project B via Workload Identity.
- Create the GSA in project A and grant it
roles/vieweron project A. - Grant
roles/iam.workloadIdentityUseron the GSA to both the project-B cluster’sworkspace-builderandrunnerKSAs — the workspace-builder runs discovery, and the runner executes generated tasks that also need GCP access:Terminal window gcloud iam service-accounts add-iam-policy-binding \"discovery-sa@project-a.iam.gserviceaccount.com" \--role="roles/iam.workloadIdentityUser" \--member="serviceAccount:project-b.svc.id.goog[runwhen-local/workspace-builder]"gcloud iam service-accounts add-iam-policy-binding \"discovery-sa@project-a.iam.gserviceaccount.com" \--role="roles/iam.workloadIdentityUser" \--member="serviceAccount:project-b.svc.id.goog[runwhen-local/runner]" - Grant
roles/vieweron project B to the same GSA (so it can discover project B’s cloud resources too):Terminal window gcloud projects add-iam-policy-binding project-b \--member="serviceAccount:discovery-sa@project-a.iam.gserviceaccount.com" \--role="roles/viewer" - Annotate both KSAs in the project-B cluster with the project-A GSA email (same Helm
serviceAccount.annotationsas above — applied to bothworkspaceBuilderandrunner). - Apply in-cluster RBAC in the project-B cluster for any Kubernetes-level access (same pattern as the targeted example above). Remember: the RBAC binds to the GSA email (
kind: User), so a single binding covers both the workspace-builder and runner pods automatically.
The GSA’s cloud-level roles/viewer on both projects lets it enumerate GCP resources and list GKE clusters. In-cluster RBAC in each cluster controls what Kubernetes resources it can actually read/write. No JSON keys are exchanged — Workload Identity handles the cross-project federation.