LAB42_GCP_Kubernetes Security: GKE with Workload Identity

Kubernetes Security: GKE with Workload Identity

☁ GCP 🔥 Intermediate GKE Workload Identity IAM Kubernetes

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.

Your Objectives
  • 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"