Skip to main content

Kubernetes to GCP - Secretless Authentication

Connect Kubernetes workloads to Google Cloud using GKE Workload Identity for secretless authentication - no service account keys required.

Overview​

This guide shows how to configure Kubernetes pods running on GKE to authenticate with Google Cloud services using Workload Identity instead of storing service account JSON keys.

Time Required: 20-30 minutes Difficulty: Intermediate

What You'll Accomplish​

  • ✓ Enable GKE Workload Identity on your cluster
  • ✓ Create a Google Cloud Service Account with appropriate permissions
  • ✓ Bind Kubernetes ServiceAccount to GCP Service Account
  • ✓ Deploy a pod that can access GCP services without credentials
  • ✓ Test and verify the end-to-end authentication flow

Benefits​

  • No Stored Keys: Zero service account JSON keys stored in Kubernetes secrets
  • Automatic Rotation: Identity tokens are generated on-demand and expire automatically
  • Fine-Grained Access: IAM policies provide precise control over GCP resource access
  • Audit Trail: Complete authentication history in Cloud Audit Logs
  • Pod-Level Security: Different pods can have different GCP permissions

Prerequisites​

GCP Requirements​

  • Google Cloud project with billing enabled
  • GKE cluster (version 1.14+ recommended)
  • gcloud CLI installed and configured
  • IAM permissions: iam.serviceAccounts.create, iam.serviceAccountKeys.create, iam.serviceAccounts.setIamPolicy

Kubernetes Requirements​

  • GKE cluster with Workload Identity enabled
  • kubectl configured to access your cluster
  • Cluster admin permissions

Knowledge Requirements​

  • Understanding of Kubernetes ServiceAccounts
  • Familiarity with GCP IAM service accounts and roles
  • Basic understanding of OIDC authentication flows

Architecture​

Implementation​

Step 1: Enable Workload Identity on GKE Cluster​

For New Clusters​

gcloud container clusters create my-cluster \
--region=us-central1 \
--workload-pool=PROJECT_ID.svc.id.goog \
--enable-autoupgrade \
--enable-autorepair

# Replace PROJECT_ID with your actual project ID

For Existing Clusters​

# Enable Workload Identity
gcloud container clusters update my-cluster \
--region=us-central1 \
--workload-pool=PROJECT_ID.svc.id.goog

# Update node pools (required for existing clusters)
gcloud container node-pools update default-pool \
--cluster=my-cluster \
--region=us-central1 \
--workload-metadata=GKE_METADATA

Verify:

gcloud container clusters describe my-cluster \
--region=us-central1 \
--format="value(workloadIdentityConfig.workloadPool)"

# Expected output: PROJECT_ID.svc.id.goog

Step 2: Create Google Cloud Service Account​

# Set variables
export PROJECT_ID="your-project-id"
export GSA_NAME="my-app-gsa"
export GSA_EMAIL="${GSA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com"

# Create service account
gcloud iam service-accounts create $GSA_NAME \
--display-name="Service account for my-app" \
--project=$PROJECT_ID

Step 3: Grant GCP Permissions​

Grant the service account permissions to access GCP resources:

# Example: Grant Cloud Storage access
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:${GSA_EMAIL}" \
--role="roles/storage.objectViewer"

# Example: Grant Compute Engine access
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:${GSA_EMAIL}" \
--role="roles/compute.viewer"

# For bucket-specific access (more secure)
gsutil iam ch serviceAccount:${GSA_EMAIL}:objectViewer gs://my-bucket

Recommended roles for common use cases:

Use CaseRoleDescription
Read Cloud Storageroles/storage.objectViewerRead objects from buckets
Write Cloud Storageroles/storage.objectCreatorCreate/update objects
BigQuery accessroles/bigquery.dataViewerQuery BigQuery datasets
Pub/Sub publishroles/pubsub.publisherPublish messages to topics
Secret Managerroles/secretmanager.secretAccessorAccess secret values

Step 4: Create Kubernetes ServiceAccount​

# serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: my-app-ksa
namespace: default
annotations:
iam.gke.io/gcp-service-account: my-app-gsa@PROJECT_ID.iam.gserviceaccount.com

Apply:

kubectl apply -f serviceaccount.yaml

Step 5: Bind Kubernetes SA to GCP SA​

Create the IAM policy binding:

# Set variables
export KSA_NAME="my-app-ksa"
export NAMESPACE="default"

# Create binding
gcloud iam service-accounts add-iam-policy-binding $GSA_EMAIL \
--role=roles/iam.workloadIdentityUser \
--member="serviceAccount:${PROJECT_ID}.svc.id.goog[${NAMESPACE}/${KSA_NAME}]"

Verify the binding:

gcloud iam service-accounts get-iam-policy $GSA_EMAIL

Expected output includes:

bindings:
- members:
- serviceAccount:PROJECT_ID.svc.id.goog[default/my-app-ksa]
role: roles/iam.workloadIdentityUser

Step 6: Deploy Pod Using ServiceAccount​

# pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: my-app-pod
namespace: default
spec:
serviceAccountName: my-app-ksa
containers:
- name: gcloud
image: google/cloud-sdk:slim
command: ["sleep", "3600"]
# GCP SDK automatically uses Workload Identity
# No additional configuration needed!

Deploy:

kubectl apply -f pod.yaml

Testing and Verification​

Verify ServiceAccount Configuration​

# Check annotation
kubectl get serviceaccount my-app-ksa -n default -o yaml | grep iam.gke.io

# Expected output:
# iam.gke.io/gcp-service-account: my-app-gsa@PROJECT_ID.iam.gserviceaccount.com

Verify IAM Binding​

gcloud iam service-accounts get-iam-policy $GSA_EMAIL \
--format="table(bindings.members)"

Test GCP Authentication​

# Get GCP identity
kubectl exec my-app-pod -- gcloud auth list

# Expected output shows GCP service account email
# * my-app-gsa@PROJECT_ID.iam.gserviceaccount.com

Test Resource Access​

# List Cloud Storage buckets
kubectl exec my-app-pod -- gcloud storage buckets list

# List Compute Engine instances
kubectl exec my-app-pod -- gcloud compute instances list

# Access a specific bucket
kubectl exec my-app-pod -- gcloud storage ls gs://my-bucket/

Expected Success Output​

✓ ServiceAccount has iam.gke.io/gcp-service-account annotation
✓ IAM binding exists for workloadIdentityUser role
✓ gcloud auth list shows GCP service account
✓ Pod can access GCP resources per IAM permissions

Production Hardening​

Security Best Practices​

1. Use Namespace Isolation​

apiVersion: v1
kind: Namespace
metadata:
name: production
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: my-app-ksa
namespace: production # Isolated namespace
annotations:
iam.gke.io/gcp-service-account: my-app-prod-gsa@PROJECT_ID.iam.gserviceaccount.com

2. Implement Least Privilege IAM​

Don't do this:

# Too broad - grants project-level owner access
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:${GSA_EMAIL}" \
--role="roles/owner"

Do this:

# Specific resource-level permissions
gsutil iam ch serviceAccount:${GSA_EMAIL}:objectViewer gs://specific-bucket

3. Use Custom IAM Roles​

Create custom roles with minimal permissions:

# Create custom role YAML
cat > custom-role.yaml <<EOF
title: "My App Custom Role"
description: "Custom role for my-app with minimal permissions"
stage: "GA"
includedPermissions:
- storage.buckets.get
- storage.objects.get
- storage.objects.list
EOF

# Create role
gcloud iam roles create myAppRole \
--project=$PROJECT_ID \
--file=custom-role.yaml

# Bind custom role
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:${GSA_EMAIL}" \
--role="projects/$PROJECT_ID/roles/myAppRole"

4. Enable Audit Logging​

# Enable Cloud Audit Logs for IAM
gcloud logging read "protoPayload.serviceName=\"iam.googleapis.com\"" \
--limit=10 \
--format=json

Set up log-based metrics and alerts:

# Create alert for unusual service account activity
gcloud alpha monitoring policies create \
--notification-channels=CHANNEL_ID \
--display-name="Unusual Workload Identity Activity" \
--condition-display-name="High SA token requests" \
--condition-threshold-value=100 \
--condition-threshold-duration=300s
# Bind to specific namespace and ServiceAccount
gcloud iam service-accounts add-iam-policy-binding $GSA_EMAIL \
--role=roles/iam.workloadIdentityUser \
--member="serviceAccount:${PROJECT_ID}.svc.id.goog[production/my-app-ksa]"

# Verify no wildcards are used
gcloud iam service-accounts get-iam-policy $GSA_EMAIL \
| grep -E "\[.*\*.*\]" && echo "WARNING: Wildcard binding found!"

Troubleshooting​

Issue: Pod Cannot Access GCP Resources​

Symptoms:

  • Error: "Default credentials error"
  • Error: "Application Default Credentials not found"

Solutions:

  1. Verify Workload Identity is Enabled:

    gcloud container clusters describe my-cluster \
    --region=us-central1 \
    --format="value(workloadIdentityConfig.workloadPool)"
  2. Check Node Pool Configuration:

    gcloud container node-pools describe default-pool \
    --cluster=my-cluster \
    --region=us-central1 \
    --format="value(config.workloadMetadataConfig.mode)"

    # Should return: GKE_METADATA
  3. Verify ServiceAccount Annotation:

    kubectl get sa my-app-ksa -n default -o jsonpath='{.metadata.annotations.iam\.gke\.io/gcp-service-account}'
  4. Check IAM Binding:

    gcloud iam service-accounts get-iam-policy $GSA_EMAIL \
    --format=json | jq '.bindings[] | select(.role=="roles/iam.workloadIdentityUser")'

Issue: Access Denied to GCP Resources​

Symptoms:

  • Authentication succeeds but operations fail
  • Error: "Permission denied" or "403 Forbidden"

Solutions:

  1. Verify GCP Service Account Permissions:

    gcloud projects get-iam-policy $PROJECT_ID \
    --flatten="bindings[].members" \
    --filter="bindings.members:serviceAccount:${GSA_EMAIL}" \
    --format="table(bindings.role)"
  2. Test Specific Permission:

    # Test if SA can access bucket
    gcloud storage buckets get-iam-policy gs://my-bucket \
    --flatten="bindings[].members" \
    --filter="bindings.members:serviceAccount:${GSA_EMAIL}"
  3. Grant Missing Permissions:

    # Add required role
    gcloud projects add-iam-policy-binding $PROJECT_ID \
    --member="serviceAccount:${GSA_EMAIL}" \
    --role="roles/storage.objectViewer"

Issue: Metadata Server Not Accessible​

Symptoms:

  • Error: "Could not reach metadata server"
  • Timeout connecting to 169.254.169.254

Solutions:

  1. Verify Pod is on Workload Identity-enabled Node:

    kubectl get pod my-app-pod -o wide
    # Check which node it's on

    kubectl describe node NODE_NAME | grep workload-metadata
    # Should show: GKE_METADATA
  2. Recreate Node Pool with Workload Identity:

    gcloud container node-pools create new-pool \
    --cluster=my-cluster \
    --region=us-central1 \
    --workload-metadata=GKE_METADATA
  3. Migrate Pods to New Node Pool:

    kubectl cordon NODE_NAME
    kubectl drain NODE_NAME --ignore-daemonsets --delete-emptydir-data

Complete Example​

Complete Terraform Configuration​

# complete-workload-identity-setup.tf

terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "~> 5.0"
}
kubernetes = {
source = "hashicorp/kubernetes"
version = "~> 2.0"
}
}
}

variable "project_id" {
description = "GCP Project ID"
type = string
}

variable "region" {
description = "GCP Region"
type = string
default = "us-central1"
}

variable "cluster_name" {
description = "GKE Cluster Name"
type = string
default = "my-cluster"
}

provider "google" {
project = var.project_id
region = var.region
}

# Get GKE cluster credentials
data "google_client_config" "default" {}

data "google_container_cluster" "cluster" {
name = var.cluster_name
location = var.region
}

provider "kubernetes" {
host = "https://${data.google_container_cluster.cluster.endpoint}"
token = data.google_client_config.default.access_token
cluster_ca_certificate = base64decode(data.google_container_cluster.cluster.master_auth[0].cluster_ca_certificate)
}

# Create GCP Service Account
resource "google_service_account" "my_app" {
account_id = "my-app-gsa"
display_name = "Service Account for my-app"
project = var.project_id
}

# Grant GCP permissions
resource "google_project_iam_member" "my_app_storage" {
project = var.project_id
role = "roles/storage.objectViewer"
member = "serviceAccount:${google_service_account.my_app.email}"
}

# Create Kubernetes ServiceAccount
resource "kubernetes_service_account" "my_app" {
metadata {
name = "my-app-ksa"
namespace = "default"
annotations = {
"iam.gke.io/gcp-service-account" = google_service_account.my_app.email
}
}
}

# Bind K8s SA to GCP SA
resource "google_service_account_iam_member" "workload_identity_binding" {
service_account_id = google_service_account.my_app.name
role = "roles/iam.workloadIdentityUser"
member = "serviceAccount:${var.project_id}.svc.id.goog[default/${kubernetes_service_account.my_app.metadata[0].name}]"
}

# Deploy test pod
resource "kubernetes_pod" "my_app_test" {
metadata {
name = "my-app-test-pod"
namespace = "default"
}

spec {
service_account_name = kubernetes_service_account.my_app.metadata[0].name

container {
name = "gcloud"
image = "google/cloud-sdk:slim"
command = ["sleep", "3600"]
}
}
}

# Outputs
output "gcp_service_account_email" {
value = google_service_account.my_app.email
}

output "kubernetes_service_account_name" {
value = kubernetes_service_account.my_app.metadata[0].name
}

Apply:

terraform init
terraform plan -var="project_id=YOUR_PROJECT_ID"
terraform apply -var="project_id=YOUR_PROJECT_ID"

Complete Shell Script​

#!/bin/bash
# complete-workload-identity-setup.sh

set -e

PROJECT_ID="your-project-id"
REGION="us-central1"
CLUSTER_NAME="my-cluster"
GSA_NAME="my-app-gsa"
KSA_NAME="my-app-ksa"
NAMESPACE="default"

echo "Step 1: Enabling Workload Identity on cluster..."
gcloud container clusters update $CLUSTER_NAME \
--region=$REGION \
--workload-pool=${PROJECT_ID}.svc.id.goog

echo "Step 2: Updating node pool..."
gcloud container node-pools update default-pool \
--cluster=$CLUSTER_NAME \
--region=$REGION \
--workload-metadata=GKE_METADATA

echo "Step 3: Creating GCP Service Account..."
gcloud iam service-accounts create $GSA_NAME \
--display-name="Service account for my-app" \
--project=$PROJECT_ID || echo "SA may already exist"

GSA_EMAIL="${GSA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com"
echo "✓ GCP SA Email: $GSA_EMAIL"

echo "Step 4: Granting GCP permissions..."
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:${GSA_EMAIL}" \
--role="roles/storage.objectViewer"

echo "Step 5: Creating Kubernetes ServiceAccount..."
kubectl create serviceaccount $KSA_NAME -n $NAMESPACE --dry-run=client -o yaml | \
kubectl annotate --local -f - -o yaml \
iam.gke.io/gcp-service-account=$GSA_EMAIL | \
kubectl apply -f -

echo "Step 6: Binding K8s SA to GCP SA..."
gcloud iam service-accounts add-iam-policy-binding $GSA_EMAIL \
--role=roles/iam.workloadIdentityUser \
--member="serviceAccount:${PROJECT_ID}.svc.id.goog[${NAMESPACE}/${KSA_NAME}]"

echo "Step 7: Deploying test pod..."
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: my-app-test-pod
namespace: $NAMESPACE
spec:
serviceAccountName: $KSA_NAME
containers:
- name: gcloud
image: google/cloud-sdk:slim
command: ["sleep", "3600"]
EOF

echo ""
echo "==================================="
echo "Setup Complete!"
echo "==================================="
echo ""
echo "Test the integration:"
echo " kubectl exec my-app-test-pod -- gcloud auth list"
echo " kubectl exec my-app-test-pod -- gcloud storage buckets list"

Next Steps​

Expand Your Implementation​

  • Multiple environments: Create separate ServiceAccounts and GCP SAs for dev/staging/production
  • Cross-project access: Configure IAM bindings for accessing resources in other GCP projects
  • Different GCP services: Grant permissions for BigQuery, Pub/Sub, Cloud SQL, etc.
  • Monitoring: Set up Cloud Monitoring dashboards for Workload Identity usage

Learn More​

Additional Resources​

Official Documentation​

Tools​

Blog Posts​