Kubernetes Security: GKE with Workload Identity
Scenario
In a GKE cluster, the worker nodes (Compute Engine VMs) have a Service Account. Historically, all pods running on those nodes inherited the node's Service Account privileges, violating Least Privilege. GKE Workload Identity fixes this. It allows you to map a specific Kubernetes Service Account (KSA) so that it securely impersonates a specific Google Cloud Service Account (GSA). Let's deploy a standard GKE cluster with this enabled and configure the IAM bindings.
- Provision a VPC Network and Subnet.
- Provision a google_container_cluster (Standard, not Autopilot). Enable the workload_identity_config block.
- Provision a google_container_node_pool. Enable workload_metadata_config { mode = "GKE_METADATA" }.
- Provision a google_service_account (the GSA) that the pod will assume.
- Create the IAM binding allowing the `[NAMESPACE]/[KSA_NAME]` to impersonate the GSA using the roles/iam.workloadIdentityUser role.
Additional Context
How it works: GKE intercepts metadata server requests (`http://169.254.169.254/...`) originating from the Pod. Instead of returning the Node's service account credentials, GKE requests short-lived tokens for the GSA mapped to that pod's specific KSA, and returns those securely to the pod.
Note on Kubernetes Provider: To complete the loop, you must actually create the KSA inside Kubernetes and annotate it (`iam.gke.io/gcp-service-account: GSA_EMAIL`). In this lab, we use Terraform to create the GCP infrastructure, and we demonstrate providing the K8s manifest via CLI workflow.
variables.tf
variable "project_id" { type = string }
variable "gcp_region" { type = string; default = "us-central1" }main.tf
provider "google" {
project = var.project_id
region = var.gcp_region
}
# 1. Custom VPC
resource "google_compute_network" "gke_vpc" {
name = "gke-vpc"
auto_create_subnetworks = false
}
resource "google_compute_subnetwork" "gke_subnet" {
name = "gke-subnet"
network = google_compute_network.gke_vpc.id
region = var.gcp_region
cidr_range = "10.10.0.0/24"
}
# 2. GKE Standard Cluster
resource "google_container_cluster" "primary" {
name = "wi-cluster"
location = var.gcp_region
network = google_compute_network.gke_vpc.id
subnetwork = google_compute_subnetwork.gke_subnet.id
# We can't create a cluster with no node pool defined, but we want to only use
# separately managed node pools. So we create the smallest possible default
# node pool and immediately delete it.
remove_default_node_pool = true
initial_node_count = 1
# Explicitly enable Workload Identity for the cluster
workload_identity_config {
workload_pool = "${var.project_id}.svc.id.goog"
}
}
# 3. Separately Managed Node Pool
resource "google_container_node_pool" "primary_nodes" {
name = "primary-node-pool"
location = var.gcp_region
cluster = google_container_cluster.primary.name
node_count = 1
node_config {
machine_type = "e2-medium"
# GKE_METADATA enables Workload Identity enforcement on nodes
workload_metadata_config {
mode = "GKE_METADATA"
}
# Best Practice: Give the nodes a dummy SA with zero permissions so pods that
# DON'T use workload identity crash instead of inheriting high privileges.
# (Assuming you created a blank SA called 'gke-node-sa' beforehand)
}
}
# 4. Applications Google Service Account (GSA)
resource "google_service_account" "app_gsa" {
account_id = "my-app-db-reader"
display_name = "GSA for my-app to read Cloud SQL"
}
# (Give this GSA permission to do something inside GCP)
# e.g., Cloud SQL Client role...
# 5. Workload Identity IAM Binding
resource "google_service_account_iam_member" "gke_wi_binding" {
service_account_id = google_service_account.app_gsa.name
role = "roles/iam.workloadIdentityUser"
# Let the Kubernetes Service Account ("my-app-ksa") in the "default" namespace assume this identity
member = "serviceAccount:${var.project_id}.svc.id.goog[default/my-app-ksa]"
}Workflow Commands
terraform init
terraform apply -var="project_id=my-gcp-project-123"
# 1. Connect to cluster natively
gcloud container clusters get-credentials wi-cluster --region us-central1
# 2. Create the KSA
kubectl create serviceaccount my-app-ksa --namespace default
# 3. Annotate the KSA tying it back to the GSA
kubectl annotate serviceaccount my-app-ksa \
iam.gke.io/gcp-service-account=my-app-db-reader@my-gcp-project-123.iam.gserviceaccount.com
# Deploy your pods specifically setting `serviceAccountName: my-app-ksa` in your PodSpec!
terraform destroy -var="project_id=my-gcp-project-123"
