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:
- Cluster infrastructure layer: Private nodes, shielded VMs, encrypted etcd, authorized master networks
- Cluster authentication layer: Workload Identity, RBAC, service account minimization
- Workload admission layer: Pod Security Standards, Binary Authorization, Admission Controllers
- 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: truehostIPC: truehostNetwork: trueprivileged: truehostPortexposure/procmounts with write access
With restricted enforced, additionally required:
securityContext.runAsNonRoot: truesecurityContext.allowPrivilegeEscalation: false- All capabilities dropped:
capabilities.drop: ["ALL"] seccompProfile.type: RuntimeDefaultorLocalhost
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.