Service Account Binding¶
Link Kubernetes ServiceAccounts to GCP service accounts through IAM bindings. This establishes the trust relationship for Workload Identity.
Configure Kubernetes ServiceAccount¶
Create a Kubernetes ServiceAccount in your namespace:
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: app-sa
namespace: production
annotations:
iam.gke.io/gcp-service-account: app-gcp@PROJECT_ID.iam.gserviceaccount.com
The annotation links the Kubernetes ServiceAccount to a GCP service account.
Annotation Key
The annotation key is iam.gke.io/gcp-service-account. This tells GKE which GCP service account the pod should impersonate.
Create GCP Service Account¶
# Create service account in GCP
gcloud iam service-accounts create app-gcp \
--display-name "App workload identity"
# Grant it the necessary role(s)
gcloud projects add-iam-policy-binding PROJECT_ID \
--member="serviceAccount:app-gcp@PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/storage.objectViewer"
This service account holds the actual permissions. The Kubernetes ServiceAccount only acts as this account.
Least Privilege
Grant only the permissions the workload needs. Avoid roles/editor or roles/owner.
Bind Kubernetes ServiceAccount to GCP Service Account¶
# Allow the Kubernetes ServiceAccount to impersonate the GCP service account
gcloud iam service-accounts add-iam-policy-binding \
app-gcp@PROJECT_ID.iam.gserviceaccount.com \
--role="roles/iam.workloadIdentityUser" \
--member="serviceAccount:PROJECT_ID.svc.id.goog[production/app-sa]"
Format: serviceAccount:PROJECT_ID.svc.id.goog[NAMESPACE/KSA_NAME]
This is the crucial binding. It declares that the Kubernetes ServiceAccount production/app-sa can impersonate app-gcp@PROJECT_ID.iam.gserviceaccount.com.
Binding Format
The member format is critical:
serviceAccount:prefixPROJECT_ID.svc.id.goog(workload pool)[NAMESPACE/KSA_NAME](Kubernetes namespace and ServiceAccount name)
Terraform Configuration¶
For infrastructure-as-code:
# Create GCP service account
resource "google_service_account" "app" {
account_id = "app-gcp"
display_name = "App workload identity"
project = var.project_id
}
# Grant permissions to service account
resource "google_project_iam_member" "app_storage_viewer" {
project = var.project_id
role = "roles/storage.objectViewer"
member = "serviceAccount:${google_service_account.app.email}"
}
# Allow Kubernetes ServiceAccount to impersonate GCP service account
resource "google_service_account_iam_member" "app_workload_identity_user" {
service_account_id = google_service_account.app.name
role = "roles/iam.workloadIdentityUser"
member = "serviceAccount:${var.project_id}.svc.id.goog[production/app-sa]"
}
# Output the GCP service account email for Kubernetes annotation
output "gcp_service_account_email" {
value = google_service_account.app.email
}
Use this Terraform to create the GCP side. Then reference the output in your Kubernetes manifests:
# Get the service account email from Terraform
export GCP_SA_EMAIL=$(terraform output -raw gcp_service_account_email)
# Create Kubernetes ServiceAccount with annotation
kubectl create serviceaccount app-sa -n production
kubectl annotate serviceaccount app-sa -n production \
"iam.gke.io/gcp-service-account=${GCP_SA_EMAIL}"
Multiple Namespace Bindings¶
One GCP service account can be used by multiple Kubernetes namespaces:
# Bind to namespace 'production'
gcloud iam service-accounts add-iam-policy-binding \
app-gcp@PROJECT_ID.iam.gserviceaccount.com \
--role="roles/iam.workloadIdentityUser" \
--member="serviceAccount:PROJECT_ID.svc.id.goog[production/app-sa]"
# Bind to namespace 'staging'
gcloud iam service-accounts add-iam-policy-binding \
app-gcp@PROJECT_ID.iam.gserviceaccount.com \
--role="roles/iam.workloadIdentityUser" \
--member="serviceAccount:PROJECT_ID.svc.id.goog[staging/app-sa]"
Shared Permissions
All namespaces using the same GCP service account share its permissions. Consider separate service accounts per environment.
Resource-Level IAM¶
Grant access to specific resources instead of project-wide:
# Grant access only to a specific GCS bucket
gcloud storage buckets add-iam-policy-binding gs://sensitive-data \
--member="serviceAccount:app-gcp@PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/storage.objectViewer"
# Grant access to a specific BigQuery dataset
bq add-iam-policy-binding \
--member="serviceAccount:app-gcp@PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/bigquery.dataViewer" \
PROJECT_ID:dataset_name
# Grant access to specific Secret Manager secrets
gcloud secrets add-iam-policy-binding secret-name \
--member="serviceAccount:app-gcp@PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor"
Cross-Project Bindings¶
Allow service accounts from one project to access resources in another:
# In PROJECT_A: Create service account
gcloud iam service-accounts create app-a \
--project PROJECT_A \
--display-name "App in PROJECT_A"
# In PROJECT_B: Grant permissions
gcloud projects add-iam-policy-binding PROJECT_B \
--member="serviceAccount:app-a@PROJECT_A.iam.gserviceaccount.com" \
--role="roles/storage.objectViewer"
# In PROJECT_A: Bind to Kubernetes ServiceAccount
gcloud iam service-accounts add-iam-policy-binding \
app-a@PROJECT_A.iam.gserviceaccount.com \
--role="roles/iam.workloadIdentityUser" \
--member="serviceAccount:PROJECT_A.svc.id.goog[production/app-sa]"
Service Account Impersonation¶
One service account can impersonate another:
# SERVICE_ACCOUNT_A impersonates SERVICE_ACCOUNT_B
gcloud iam service-accounts add-iam-policy-binding \
service-account-b@PROJECT_ID.iam.gserviceaccount.com \
--role="roles/iam.serviceAccountTokenCreator" \
--member="serviceAccount:service-account-a@PROJECT_ID.iam.gserviceaccount.com"
Use this for temporary privilege escalation or cross-project access.
Verification¶
# 1. Verify ServiceAccount is annotated
kubectl get serviceaccount app-sa -n production -o yaml \
| grep gcp-service-account
# 2. Verify IAM binding exists
gcloud iam service-accounts get-iam-policy \
app-gcp@PROJECT_ID.iam.gserviceaccount.com
# Expected output includes:
# - role: roles/iam.workloadIdentityUser
# members:
# - serviceAccount:PROJECT_ID.svc.id.goog[production/app-sa]
# 3. Test from pod
kubectl run -it --rm debug \
--image=google/cloud-sdk:slim \
--serviceaccount=app-sa \
--namespace=production \
-- gcloud auth list
# Expected output: app-gcp@PROJECT_ID.iam.gserviceaccount.com
Expected Output
The gcloud auth list command should show the GCP service account email, not the Compute Engine default service account.
Audit IAM Bindings¶
# List all Workload Identity bindings for a service account
gcloud iam service-accounts get-iam-policy \
app-gcp@PROJECT_ID.iam.gserviceaccount.com \
--filter="bindings.role:roles/iam.workloadIdentityUser" \
--format="table(bindings.role, bindings.members)"
# List all service accounts with Workload Identity bindings
gcloud iam service-accounts list \
--project=PROJECT_ID \
--format="table(email, displayName)" | while read sa; do
echo "=== $sa ==="
gcloud iam service-accounts get-iam-policy "$sa" \
--filter="bindings.role:roles/iam.workloadIdentityUser" \
--format="yaml"
done
Remove Bindings¶
# Remove Workload Identity binding
gcloud iam service-accounts remove-iam-policy-binding \
app-gcp@PROJECT_ID.iam.gserviceaccount.com \
--role="roles/iam.workloadIdentityUser" \
--member="serviceAccount:PROJECT_ID.svc.id.goog[production/app-sa]"
# Delete GCP service account (revokes all access immediately)
gcloud iam service-accounts delete app-gcp@PROJECT_ID.iam.gserviceaccount.com
Related Configuration¶
- Cluster Configuration - Enable Workload Identity on GKE clusters
- Pod Configuration - Deploy workloads and common access patterns
- Troubleshooting - Debug IAM binding issues