Prefer Using Dedicated Gcp Service Accounts And Workload
More Info:
Workload Identity eliminates the need for developers to manage credentials. It lets a Kubernetes service account act as a dedicated Google service account, so a workload authenticates to Google Cloud APIs with short-lived, automatically rotated credentials instead of an exported service account key. Workloads that share the node service account inherit every permission the node pool holds, so each workload should get its own Google service account bound to its Kubernetes service account.
Risk Level
High
Address
Security
Compliance Standards
- APRA CPS 234 (Australia)
- BSI C5 (Germany)
- Brazil LGPD
- CCPA / CPRA (California)
- CIS Critical Security Controls v8
- CIS GKE
- CMMC 2.0
- CSA Cloud Controls Matrix v4
- DPDPA
- Digital Operational Resilience Act (EU)
- Essential 8
- ISO/IEC 27017
- ISO/IEC 27018
- ISO/IEC 27701
- KSA PDPL
- MAS Technology Risk Management (Singapore)
- MITRE ATT&CK (Cloud)
- NIST SP 800-171
- NYDFS 23 NYCRR 500
- SWIFT Customer Security Controls Framework
- Sarbanes-Oxley IT General Controls
- UK NCSC Cyber Assessment Framework
Triage and Remediation
- Remediation
Remediation
Manual Steps
-
Confirm cluster-level Workload Identity configuration
- Run on any machine with
gcloudaccess:gcloud container clusters describe CLUSTER_NAME \--location LOCATION \--project PROJECT_ID \--format=json | jq '.workloadIdentityConfig' - If the output is
nullor missingworkloadPool, plan to enable Workload Identity on the cluster before proceeding with per-workload changes.
- Run on any machine with
-
Check node pool workload metadata mode and plan migration
- List node pools and their workload metadata mode:
gcloud container node-pools list \--cluster CLUSTER_NAME \--location LOCATION \--project PROJECT_ID \--format='table(name, config.workloadMetadataConfig.mode)'
- For node pools not using
GKE_METADATA, plan to:- Either update them in-place (which will restart nodes):
gcloud container node-pools update NODE_POOL_NAME \--cluster CLUSTER_NAME \--location LOCATION \--project PROJECT_ID \--workload-metadata=GKE_METADATA
- Or create new node pools with
--workload-metadata=GKE_METADATAand migrate workloads to them for lower-impact rollout.
- Either update them in-place (which will restart nodes):
- List node pools and their workload metadata mode:
-
Identify workloads using GCP APIs and their current auth method
- List pods and namespaces to scope which ones likely call GCP APIs (e.g., by labels, images, or application knowledge):
kubectl get pods -A -o wide
- For candidate workloads, inspect pod specs for service account usage and any references to key files or env vars (e.g.,
GOOGLE_APPLICATION_CREDENTIALS):kubectl get deploy -n NAMESPACE DEPLOYMENT_NAME -o yaml - Note which Kubernetes service accounts these pods use and whether they rely on node identity or static JSON key files.
- List pods and namespaces to scope which ones likely call GCP APIs (e.g., by labels, images, or application knowledge):
-
Design dedicated GSA ↔ KSA mappings per workload
- For each distinct workload type that needs GCP access:
- Decide a dedicated Google service account (GSA) name, one per workload or tightly scoped permission boundary.
gcloud iam service-accounts create GSA_NAME \--project PROJECT_ID \--description="For workload X in namespace Y" \--display-name="GSA for workload X"
- Grant only required IAM roles to each GSA:
gcloud projects add-iam-policy-binding PROJECT_ID \--member="serviceAccount:GSA_NAME@PROJECT_ID.iam.gserviceaccount.com" \--role="roles/ROLE_NEEDED"
- Decide a dedicated Google service account (GSA) name, one per workload or tightly scoped permission boundary.
- Document which KSA (namespace/name) should map to which dedicated GSA and verify there is no reuse across unrelated workloads.
- For each distinct workload type that needs GCP access:
-
Bind KSAs to GSAs via Workload Identity and update workloads
- For each workload:
- Annotate the Kubernetes service account to impersonate the dedicated GSA:
kubectl annotate serviceaccount KSA_NAME \--namespace NAMESPACE \iam.gke.io/gcp-service-account=GSA_NAME@PROJECT_ID.iam.gserviceaccount.com \--overwrite
- Allow the KSA to impersonate the GSA:
gcloud iam service-accounts add-iam-policy-binding \GSA_NAME@PROJECT_ID.iam.gserviceaccount.com \--project PROJECT_ID \--member="serviceAccount:PROJECT_ID.svc.id.goog[NAMESPACE/KSA_NAME]" \--role="roles/iam.workloadIdentityUser"
- Ensure the pod spec explicitly uses the intended KSA and no longer mounts or references static key files or
GOOGLE_APPLICATION_CREDENTIALSpointing to JSON keys. Update manifests accordingly and redeploy.
- Annotate the Kubernetes service account to impersonate the dedicated GSA:
- For each workload:
-
Verify workloads are using dedicated GSAs via Workload Identity
- From a running pod of each workload, check the identity returned by the metadata server:
kubectl exec -it POD_NAME -n NAMESPACE -- \curl -s -H "Metadata-Flavor: Google" \"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/email"
- Confirm the returned email matches the dedicated
GSA_NAME@PROJECT_ID.iam.gserviceaccount.comfor that workload, and that pods with different permission needs use different GSAs.
- From a running pod of each workload, check the identity returned by the metadata server:
Using kubectl
kubectl cannot configure Workload Identity or GCP service accounts, because this setting is managed at the GKE control-plane and node pool level through gcloud, the GCP console, or IaC. To address this finding, follow the guidance in the Manual Steps section, where you will enable Workload Identity on the cluster and node pools and then bind Kubernetes service accounts to dedicated Google service accounts.
Automation
#!/usr/bin/env bash
# Report Workload Identity and GCP SA usage across a GKE cluster
# Run on: any machine with gcloud, kubectl, jq, and cluster access.
set -euo pipefail
PROJECT_ID="<YOUR_PROJECT_ID>"
CLUSTERS_LOCATION="<CLUSTERS_REGION_OR_ZONE>" # e.g. us-central1
echo "=== Cluster-level Workload Identity configuration ==="
gcloud container clusters list \
--project "${PROJECT_ID}" \
--format="table(name,location,workloadIdentityConfig.workloadPool)"
echo
echo "Details per cluster:"
while read -r NAME LOCATION _; do
# skip header
[[ "${NAME}" == "NAME" ]] && continue
echo
echo "---- Cluster: ${NAME} (${LOCATION}) ----"
gcloud container clusters describe "${NAME}" \
--location "${LOCATION}" \
--project "${PROJECT_ID}" \
--format="json(workloadIdentityConfig,addonsConfig.workloadIdentityConfig)" | jq .
echo
echo "Node pools and workload metadata mode:"
gcloud container node-pools list \
--cluster "${NAME}" \
--location "${LOCATION}" \
--project "${PROJECT_ID}" \
--format="table(name,config.serviceAccount,config.workloadMetadataConfig.mode)"
done < <(gcloud container clusters list --project "${PROJECT_ID}" --format="value(name,location)")
echo
echo "=== Kubernetes service accounts annotated for Workload Identity ==="
kubectl get sa --all-namespaces -o json \
| jq -r '
.items[]
| {
ns: .metadata.namespace,
name: .metadata.name,
gsa: .metadata.annotations["iam.gke.io/gcp-service-account"]
}
| select(.gsa != null)
| "\(.ns),\(.name),\(.gsa)"' \
| awk 'BEGIN{FS=","; OFS="\t"; print "NAMESPACE","KSA","BOUND_GSA"} {print}'
echo
echo "=== Pods and their service accounts (for workload mapping review) ==="
kubectl get pods --all-namespaces -o custom-columns='NAMESPACE:.metadata.namespace,POD:.metadata.name,SA:.spec.serviceAccountName' \
| sort
echo
echo "=== Pods mounting potential GCP SA key secrets (possible misuse of keys) ==="
kubectl get pods --all-namespaces -o json \
| jq -r '
.items[]
| select(.spec.volumes != null)
| . as $pod
| .spec.volumes[]
| select(.secret != null)
| "\($pod.metadata.namespace),\($pod.metadata.name),\(.secret.secretName)"' \
| sort \
| awk 'BEGIN{FS=","; OFS="\t"; print "NAMESPACE","POD","SECRET_NAME"} {print}'
Explanation of what output indicates a problem:
-
Cluster-level Workload Identity:
- Problem:
workloadIdentityConfig.workloadPoolis empty or null for a cluster. - Desired: Non-empty value like
PROJECT_ID.svc.id.goog.
- Problem:
-
Node pool workload metadata:
- Problem:
config.workloadMetadataConfig.modeisGCE_METADATAorLEGACY_METADATA_SERVER. - Desired:
GKE_METADATAfor node pools meant to use Workload Identity.
- Problem:
-
Kubernetes service accounts (KSAs):
- Problem: Critical workloads use KSAs that:
- Have no
iam.gke.io/gcp-service-accountannotation but still call GCP APIs, or - Share the same
BOUND_GSAacross many unrelated workloads/namespaces.
- Have no
- Desired: Each distinct workload that needs GCP permissions has its own KSA with a dedicated
BOUND_GSA.
- Problem: Critical workloads use KSAs that:
-
Pods and service accounts:
- Problem: Many pods across different applications share the same KSA name (especially in
defaultnamespace), while requiring different GCP permissions. - Desired: Application- or component-specific KSAs instead of broad shared ones.
- Problem: Many pods across different applications share the same KSA name (especially in
-
Pods mounting secrets (possible key-based auth):
- Problem: Pods that access GCP APIs are mounting secrets that contain long-lived service account keys (you need to manually inspect listed
SECRET_NAMEs). - Desired: Workloads use Workload Identity without static JSON key files; such secrets should not be required.
- Problem: Pods that access GCP APIs are mounting secrets that contain long-lived service account keys (you need to manually inspect listed