GKE Security Hardening: CIS Benchmarks, Workload Identity, and Binary Authorization

A GKE cluster with default settings passes health checks but fails security audits. This guide implements CIS Kubernetes Benchmark controls, configures Workload Identity for secretless authentication, and enforces Binary Authorization to block unsigned container images in production.

Kubernetes has a reputation for being hard to secure, and that reputation is at least partially earned. The default configuration of a Kubernetes cluster — and by extension a GKE cluster with all the defaults accepted — leaves a meaningful attack surface exposed. Privileged pods can run unchallenged. Service accounts have overly broad permissions. Container images from any registry can be deployed. Network policies allow unrestricted pod-to-pod communication.

This guide works through the security controls that close those gaps on GKE. We'll follow the CIS Kubernetes Benchmark as a framework, implement Workload Identity for pod-level GCP authentication, configure Binary Authorization to enforce image signing, and set up network policies that implement least-privilege network segmentation.

This isn't an academic exercise. We'll use real Terraform and Kubernetes YAML that you can apply to an existing cluster.

Security Architecture Overview

GKE security operates in layers:

  1. Cluster infrastructure layer: Private nodes, shielded VMs, encrypted etcd, authorized master networks
  2. Cluster authentication layer: Workload Identity, RBAC, service account minimization
  3. Workload admission layer: Pod Security Standards, Binary Authorization, Admission Controllers
  4. Runtime layer: Network Policies, Security Command Center threat detection, container runtime monitoring

We'll work through each layer. If you haven't yet set up the cluster infrastructure layer, start with our GKE production cluster configuration guide first.

CIS Benchmark Controls: The Essential List

The CIS Kubernetes Benchmark has over 80 controls. Many are handled automatically by GKE. The ones you need to actively configure fall into six categories.

1. Control Plane Configuration

# Verify audit logging is enabled
gcloud container clusters describe my-cluster   --region=europe-west4   --format="value(loggingConfig)"

# Enable audit logs if not already active
gcloud container clusters update my-cluster   --region=europe-west4   --logging=SYSTEM,WORKLOAD,API_SERVER,SCHEDULER,CONTROLLER_MANAGER

The API_SERVER log component captures all Kubernetes API calls — this is your audit trail for RBAC investigations and incident response.

2. Node Configuration: Shielded VMs and Secure Boot

# In your node pool Terraform
node_config {
  shielded_instance_config {
    enable_secure_boot          = true  # Verifies boot sequence integrity
    enable_integrity_monitoring = true  # Detects boot-time tampering
  }

  # Use minimal OAuth scopes — never use cloud-platform broadly unless needed
  oauth_scopes = [
    "https://www.googleapis.com/auth/logging.write",
    "https://www.googleapis.com/auth/monitoring",
  ]
}

3. RBAC: Eliminating Dangerous Default Permissions

GKE clusters come with some RBAC bindings that violate least privilege. Audit and clean them:

# Find ClusterRoleBindings using cluster-admin (most dangerous role)
kubectl get clusterrolebindings -o json |   jq '.items[] | select(.roleRef.name == "cluster-admin") | .metadata.name'

# Review all bindings for the default service account
kubectl get rolebindings,clusterrolebindings --all-namespaces   -o json | jq '.items[] | select(.subjects[]?.name == "default")'

Never bind cluster-admin to human users or application service accounts. Create scoped roles for specific operations:

# Example: Read-only access to deployments in a specific namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: deployment-viewer
  namespace: my-app
rules:
- apiGroups: ["apps"]
  resources: ["deployments", "replicasets"]
  verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: ci-deployment-viewer
  namespace: my-app
subjects:
- kind: ServiceAccount
  name: ci-runner
  namespace: my-app
roleRef:
  kind: Role
  name: deployment-viewer
  apiGroup: rbac.authorization.k8s.io

Workload Identity: Eliminating Service Account Keys

The most common GKE security mistake is mounting service account JSON keys as Kubernetes secrets to give pods GCP permissions. Keys can be leaked, forgotten in image layers, or left active long after a pod is deleted.

Workload Identity eliminates keys entirely. A Kubernetes ServiceAccount maps to a GCP IAM service account through a federation mechanism — pods get fresh, short-lived credentials automatically.

Setting Up Workload Identity

# Step 1: Enable Workload Identity on the cluster (if not already done)
gcloud container clusters update my-cluster   --region=europe-west4   --workload-pool=my-project.svc.id.goog

# Step 2: Create a GCP IAM service account for your application
gcloud iam service-accounts create my-app-sa   --display-name="My Application SA"   --project=my-project

# Step 3: Grant the GCP SA the permissions it needs
gcloud projects add-iam-policy-binding my-project   --member="serviceAccount:my-app-sa@my-project.iam.gserviceaccount.com"   --role="roles/bigquery.dataViewer"

# Step 4: Allow the Kubernetes SA to impersonate the GCP SA
gcloud iam service-accounts add-iam-policy-binding   my-app-sa@my-project.iam.gserviceaccount.com   --role="roles/iam.workloadIdentityUser"   --member="serviceAccount:my-project.svc.id.goog[my-namespace/my-app-sa]"
# Step 5: Create the Kubernetes ServiceAccount with the annotation
apiVersion: v1
kind: ServiceAccount
metadata:
  name: my-app-sa
  namespace: my-namespace
  annotations:
    iam.gke.io/gcp-service-account: my-app-sa@my-project.iam.gserviceaccount.com
# Step 6: Reference it in your Deployment
spec:
  serviceAccountName: my-app-sa  # Pods get GCP credentials automatically
  containers:
  - name: my-app
    image: my-image:latest

Verify it works:

# Exec into a pod and verify token works
kubectl exec -it my-pod -n my-namespace --   curl -H "Metadata-Flavor: Google"   http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token

Auditing Existing Key Usage

Find all Kubernetes secrets that contain GCP service account keys:

kubectl get secrets --all-namespaces -o json |   jq -r '.items[] | select(.data | to_entries[] | .value | @base64d | startswith("{")) | "(.metadata.namespace)/(.metadata.name)"'

For each key-based secret found, migrate to Workload Identity and delete the key from IAM:

gcloud iam service-accounts keys list   --iam-account=my-app-sa@my-project.iam.gserviceaccount.com

gcloud iam service-accounts keys delete KEY_ID   --iam-account=my-app-sa@my-project.iam.gserviceaccount.com

Pod Security Standards

Kubernetes Pod Security Standards (PSS) replaced the deprecated PodSecurityPolicy. PSS operates at the namespace level and enforces three levels: Privileged (no restrictions), Baseline (prevents known escapes), and Restricted (hardened).

# Label namespaces with the enforcement level
# Production workloads: enforce baseline
kubectl label namespace my-app   pod-security.kubernetes.io/enforce=baseline   pod-security.kubernetes.io/enforce-version=latest   pod-security.kubernetes.io/warn=restricted   pod-security.kubernetes.io/warn-version=latest

# Highly sensitive namespaces: enforce restricted
kubectl label namespace payments   pod-security.kubernetes.io/enforce=restricted   pod-security.kubernetes.io/enforce-version=latest

With baseline enforced, any pod spec requesting the following is automatically rejected:

  • hostPID: true
  • hostIPC: true
  • hostNetwork: true
  • privileged: true
  • hostPort exposure
  • /proc mounts with write access

With restricted enforced, additionally required:

  • securityContext.runAsNonRoot: true
  • securityContext.allowPrivilegeEscalation: false
  • All capabilities dropped: capabilities.drop: ["ALL"]
  • seccompProfile.type: RuntimeDefault or Localhost

A compliant pod spec looks like:

spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    fsGroup: 2000
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: my-app
    image: my-image:latest
    securityContext:
      allowPrivilegeEscalation: false
      readOnlyRootFilesystem: true
      capabilities:
        drop: ["ALL"]
    resources:
      requests:
        cpu: "100m"
        memory: "128Mi"
      limits:
        cpu: "500m"
        memory: "512Mi"

Binary Authorization: Enforcing Image Signing

Binary Authorization prevents deployment of unsigned or untrusted container images. In a production supply chain security posture, every image that runs in production should be built by your CI system and signed with an attestor key.

# Step 1: Enable Binary Authorization
gcloud services enable binaryauthorization.googleapis.com

# Step 2: Create an attestor (a trusted signing authority)
gcloud container binauthz attestors create prod-attestor   --attestation-authority-note=projects/my-project/notes/prod-build-note   --attestation-authority-note-project=my-project

# Step 3: Create the note (metadata about the attestor)
cat > /tmp/note.json << 'EOF'
{
  "name": "projects/my-project/notes/prod-build-note",
  "attestation_authority": {
    "hint": {
      "human_readable_name": "Production Build Attestor"
    }
  }
}
EOF

curl -X POST   -H "Authorization: Bearer $(gcloud auth print-access-token)"   -H "Content-Type: application/json"   -d @/tmp/note.json   "https://containeranalysis.googleapis.com/v1/projects/my-project/notes?noteId=prod-build-note"

# Step 4: Generate a signing key pair
gcloud kms keyrings create binauthz-keys --location=global
gcloud kms keys create prod-attestor-key   --keyring=binauthz-keys   --location=global   --purpose=asymmetric-signing   --default-algorithm=rsa-sign-pkcs1-4096-sha512

# Step 5: Associate the key with the attestor
KEY_VERSION=$(gcloud kms keys versions list   --key=prod-attestor-key   --keyring=binauthz-keys   --location=global   --format="value(name)" | head -1)

gcloud container binauthz attestors public-keys add   --attestor=prod-attestor   --keyversion=$KEY_VERSION

Configure the Binary Authorization policy:

cat > /tmp/policy.yaml << 'EOF'
defaultAdmissionRule:
  evaluationMode: REQUIRE_ATTESTATION
  enforcementMode: ENFORCED_BLOCK_AND_AUDIT_LOG
  requireAttestationsBy:
  - projects/my-project/attestors/prod-attestor
clusterAdmissionRules:
  europe-west4.my-dev-cluster:
    evaluationMode: ALWAYS_ALLOW  # Dev cluster: no signing required
    enforcementMode: ENFORCED_BLOCK_AND_AUDIT_LOG
globalPolicyEvaluationMode: ENABLE
EOF

gcloud container binauthz policy import /tmp/policy.yaml

In your CI pipeline (Cloud Build), attest images after they're pushed:

# cloudbuild.yaml
steps:
- name: 'gcr.io/cloud-builders/docker'
  args: ['build', '-t', 'europe-west4-docker.pkg.dev/my-project/my-repo/my-app:$COMMIT_SHA', '.']

- name: 'gcr.io/cloud-builders/docker'
  args: ['push', 'europe-west4-docker.pkg.dev/my-project/my-repo/my-app:$COMMIT_SHA']

- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
  entrypoint: 'bash'
  args:
  - '-c'
  - |
    DIGEST=$(gcloud artifacts docker images describe       europe-west4-docker.pkg.dev/my-project/my-repo/my-app:$COMMIT_SHA       --format="value(image_summary.digest)")

    gcloud container binauthz attestations sign-and-create       --artifact-url="europe-west4-docker.pkg.dev/my-project/my-repo/my-app@$DIGEST"       --attestor=prod-attestor       --attestor-project=my-project       --keyversion-project=my-project       --keyversion-location=global       --keyversion-keyring=binauthz-keys       --keyversion-key=prod-attestor-key       --keyversion=1

Now any attempt to deploy an unsigned image to the production cluster fails:

# This will be rejected if the image isn't attested
kubectl run test --image=nginx:latest
# Error: admission webhook denied: Image nginx:latest denied by attestor prod-attestor

Network Policies: Least-Privilege Pod Communication

By default, pods in Kubernetes can talk to any other pod in the cluster. Network Policies implement a deny-by-default model where you explicitly allow the traffic that should flow.

# Default deny-all policy for a namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: my-app
spec:
  podSelector: {}  # Applies to all pods
  policyTypes:
  - Ingress
  - Egress
---
# Allow the frontend to talk to the backend
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: frontend-to-backend
  namespace: my-app
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: frontend
    ports:
    - protocol: TCP
      port: 8080
---
# Allow backend to reach GCP APIs (via Cloud NAT)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: backend-egress
  namespace: my-app
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
  - Egress
  egress:
  - to:
    - ipBlock:
        cidr: 10.0.0.0/8  # Internal cluster traffic
  - to:
    - ipBlock:
        cidr: 0.0.0.0/0   # External (GCP APIs, internet)
        except:
        - 169.254.169.254/32  # Block metadata server for unauthorized pods
    ports:
    - port: 443
      protocol: TCP

Security Command Center Integration

Enable Security Command Center (SCC) Standard tier to continuously scan your GKE cluster for misconfigurations:

# Enable SCC for your project
gcloud services enable securitycenter.googleapis.com

# Enable GKE Security Posture Management
gcloud container clusters update my-cluster   --region=europe-west4   --enable-security-posture   --workload-vulnerability-scanning=standard

SCC will surface findings like:

  • Privileged containers running in the cluster
  • Containers running as root
  • Missing resource limits on pods
  • Containers with writable root filesystems
  • Network policies not enforced

Review findings weekly:

gcloud scc findings list my-project   --filter="category='GKE_CONTROL_PLANE_MISCONFIGURATION' AND state='ACTIVE'"   --format="table(name,category,severity,createTime)"

Continuous Compliance Monitoring

Security hardening is not a one-time activity. Use Cloud Build or a scheduled job to run CIS Benchmark checks regularly:

# Install kube-bench (CIS Kubernetes Benchmark scanner)
kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job.yaml

# View results
kubectl logs job/kube-bench

The combination of CIS Benchmark configuration, Workload Identity, Pod Security Standards, Binary Authorization, and Network Policies creates a defense-in-depth posture that addresses the most common Kubernetes attack paths: privilege escalation through privileged containers, lateral movement through over-permissive network access, credential theft through mounted secrets, and supply chain attacks through unsigned images.

For cost implications of running a hardened cluster, see our GKE cost optimization guide. For broader GCP security posture, see our Security Command Center guide.