Menu
Back to blog
kubernetesgkecloud-native

GKE Security Best Practices (2026 Guide)

Patrick Putman·Founder, Stormbane Security
December 17, 202513 min readBirmingham, Alabama

GKE gives you a managed control plane with automatic upgrades and patching. What it doesn't give you is a secure cluster configuration out of the box. Most of the security work — RBAC, workload identity, admission control, network policies, node security — is still yours.

This is what I check on every GKE cluster I review. Not theory — the specific configurations I find missing or misconfigured in production.

1. Enable Workload Identity (and retire service account keys)

This is the most impactful change you can make. The default way of giving workloads access to GCP APIs is to mount a service account JSON key file. That key file never expires, is stored in Kubernetes Secrets (often etcd without encryption at rest), and gets backed up with everything in the namespace.

Workload Identity replaces key files with a token exchange. A pod running as a Kubernetes service account automatically exchanges that for a GCP access token — no key file anywhere.

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

# Enable on the node pool
gcloud container node-pools update default-pool \
  --cluster=my-cluster \
  --workload-metadata=GKE_METADATA
# Bind the Kubernetes SA to the GCP SA
gcloud iam service-accounts add-iam-policy-binding \
  my-service-account@PROJECT_ID.iam.gserviceaccount.com \
  --role=roles/iam.workloadIdentityUser \
  --member="serviceAccount:PROJECT_ID.svc.id.goog[NAMESPACE/KSA_NAME]"

# Annotate the Kubernetes service account
kubectl annotate serviceaccount KSA_NAME \
  --namespace=NAMESPACE \
  iam.gke.io/gcp-service-account=my-service-account@PROJECT_ID.iam.gserviceaccount.com

After enabling Workload Identity, block metadata endpoint access from pods that don't need it. The 169.254.169.254 endpoint returns the node's service account credentials — reachable from any pod unless you explicitly block it:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: block-metadata-endpoint
spec:
  podSelector: {}
  policyTypes: [Egress]
  egress:
  - to:
    - ipBlock:
        cidr: 0.0.0.0/0
        except:
        - 169.254.169.254/32
Audit for existing key files before migrating

Before enabling Workload Identity, find existing key files mounted as secrets: kubectl get secrets -A -o json | jq '.items[] | select(.data | keys[] | test("key|credential|json"))'. Migrate those workloads before revoking the keys.

2. Enable Shielded Nodes

Shielded Nodes add three security properties to GKE VMs:

  • Secure Boot — only authenticated OS boot components load; prevents rootkits that modify the boot chain
  • vTPM — provides a hardware root of trust for measured boot
  • Integrity Monitoring — baseline boot measurement with alerts on changes
gcloud container node-pools update default-pool \
  --cluster=my-cluster \
  --enable-shielded-nodes \
  --shielded-secure-boot \
  --shielded-integrity-monitoring

In Terraform:

resource "google_container_node_pool" "primary" {
  node_config {
    shielded_instance_config {
      enable_secure_boot          = true
      enable_integrity_monitoring = true
    }
  }
}

Near-zero performance impact. No good reason not to enable it on any cluster handling sensitive workloads.

3. Configure audit logging

Without Cloud Audit Logs, you can't answer "who modified that RBAC binding?" or "who deleted that secret?" after an incident. Enable all three log types:

resource "google_project_iam_audit_config" "gke_audit" {
  project = var.project_id
  service = "container.googleapis.com"

  audit_log_config {
    log_type = "ADMIN_READ"
  }
  audit_log_config {
    log_type = "DATA_READ"
  }
  audit_log_config {
    log_type = "DATA_WRITE"
  }
}

What to alert on in those logs:

  • ClusterRoleBinding create/update/delete
  • Secret access events in production namespaces
  • Pod exec and attach operations (pods/exec, pods/attach)
  • Service account token creation

4. Network policies with deny-all defaults

By default, every pod in a GKE cluster can reach every other pod. If a workload is compromised, the attacker gets free lateral movement to everything else on the cluster network.

The baseline: a NetworkPolicy that denies all ingress and egress, with explicit allow rules per workload.

# Default deny in every namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress

Add explicit egress for DNS (required for any pod to function):

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: production
spec:
  podSelector: {}
  policyTypes: [Egress]
  egress:
  - ports:
    - port: 53
      protocol: UDP
    - port: 53
      protocol: TCP

Then explicit policies per workload:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-server-policy
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: api-server
  policyTypes: [Ingress, Egress]
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: frontend
    ports:
    - port: 8080
  egress:
  - to:
    - podSelector:
        matchLabels:
          app: database
    ports:
    - port: 5432

Verify network policy enforcement is enabled:

gcloud container clusters describe my-cluster \
  --format='get(networkConfig.datapathProvider)'
# ADVANCED_DATAPATH confirms Dataplane V2 (built-in network policy)

5. Binary Authorization for image provenance

Binary Authorization enforces that only images meeting your policy can run on the cluster. At minimum, this prevents arbitrary public images from running as footholds.

Start simple — an allow-list that restricts images to your own registry:

defaultAdmissionRule:
  evaluationMode: ALWAYS_DENY
  enforcementMode: ENFORCED_BLOCK_AND_AUDIT_LOG
admissionWhitelistPatterns:
- namePattern: us-docker.pkg.dev/my-project/*
- namePattern: gcr.io/my-project/*
# Required system images
- namePattern: gke.gcr.io/*
- namePattern: registry.k8s.io/*
# Enable on the cluster
gcloud container clusters update my-cluster \
  --enable-binauthz

# Import the policy
gcloud container binauthz policy import policy.yaml

Even this simple policy prevents someone from running docker.io/kalilinux/kali-rolling on your cluster.

6. RBAC hardening (GKE-specific)

I covered RBAC misconfigs in depth. A few GKE-specific additions:

Check for node service account bindings to cluster-admin:

kubectl get clusterrolebindings -o json | \
  jq -r '.items[] | select(.roleRef.name == "cluster-admin") | "\(.metadata.name): \(.subjects)"'

Verify Workload Metadata is GKE_METADATA (not EXPOSE or unset):

gcloud container node-pools list \
  --cluster=my-cluster \
  --format='table(name,config.workloadMetadataConfig.mode)'
# Should show GKE_METADATA for all pools

Disable the Kubernetes dashboard if present:

kubectl get deployment kubernetes-dashboard -n kube-system 2>/dev/null
kubectl get deployment kubernetes-dashboard -n kubernetes-dashboard 2>/dev/null

If it's there and unused, delete it. The dashboard is an unauthenticated API proxy risk if its RBAC is misconfigured.

7. Enable node auto-upgrade

GKE patches node OS vulnerabilities. Disabled auto-upgrade means accumulating unpatched nodes:

gcloud container node-pools update default-pool \
  --cluster=my-cluster \
  --enable-autoupgrade \
  --enable-autorepair

Set a maintenance window to control timing:

gcloud container clusters update my-cluster \
  --maintenance-window-start=2026-01-01T02:00:00Z \
  --maintenance-window-end=2026-01-01T06:00:00Z \
  --maintenance-window-recurrence="FREQ=WEEKLY;BYDAY=SU"

Quick GKE security audit

CLUSTER=my-cluster
REGION=us-central1

echo "=== Workload Identity ==="
gcloud container clusters describe $CLUSTER --region $REGION \
  --format='get(workloadIdentityConfig.workloadPool)'

echo ""
echo "=== Shielded Nodes ==="
gcloud container node-pools list --cluster=$CLUSTER --region=$REGION \
  --format='table(name,config.shieldedInstanceConfig.enableSecureBoot,config.shieldedInstanceConfig.enableIntegrityMonitoring)'

echo ""
echo "=== Binary Authorization ==="
gcloud container clusters describe $CLUSTER --region $REGION \
  --format='get(binaryAuthorization.enabled)'

echo ""
echo "=== Node auto-upgrade ==="
gcloud container node-pools list --cluster=$CLUSTER --region=$REGION \
  --format='table(name,management.autoUpgrade,management.autoRepair)'

echo ""
echo "=== ClusterAdmin bindings ==="
kubectl get clusterrolebindings -o json | \
  jq -r '.items[] | select(.roleRef.name == "cluster-admin") | "\(.metadata.name): \(.subjects)"'

echo ""
echo "=== Service account key files in secrets ==="
kubectl get secrets -A -o json | \
  jq -r '.items[] | select(.data | keys[] | test("key|credential|json")) | "\(.metadata.namespace)/\(.metadata.name)"'

None of these require additional tooling beyond gcloud and kubectl. The configuration state is all in the cluster.

If you want a systematic GKE security review — covering Workload Identity configuration, RBAC, network policy coverage, supply chain trust, and attack path mapping — that's what our Kubernetes security service covers.

Need help with this? View our Kubernetes security services or get in touch.

Related posts