Skip to main content

Consider Firewalling GKE Worker Nodes

More Info:

Apply firewall rules that restrict traffic to and from GKE worker nodes based on tags, service accounts, and CIDR ranges. Tight firewalling limits lateral movement and unwanted access.

Risk Level

Medium

Address

Security

Compliance Standards

  • CIS GKE

Triage and Remediation

Remediation

Manual Steps
  1. Identify all node pools and instance groups

    • On any machine with gcloud configured, list GKE node pools and note the node locations:
      gcloud container clusters describe CLUSTER_NAME \
      --zone ZONE \
      --project PROJECT_ID \
      --format="json(nodePools[].{name:name,config:config})"
    • List managed instance groups backing the node pools:
      gcloud compute instance-groups managed list \
      --project PROJECT_ID \
      --format="table(name,zone,baseInstanceName)"
  2. Gather current worker node tags, service accounts, and networks

    • For each node pool MIG/instance name pattern, describe a representative node to see its tags, service accounts, and network:
      gcloud compute instances list \
      --project PROJECT_ID \
      --filter="name~'BASE_INSTANCE_NAME'" \
      --format="table(name,zone)" # pick an instance name/zone

      gcloud compute instances describe INSTANCE_NAME \
      --zone ZONE \
      --project PROJECT_ID \
      --format=json | jq '{name:.name, tags:.tags.items, serviceAccounts:[.serviceAccounts[].email], networks:[.networkInterfaces[].network], subnets:[.networkInterfaces[].subnetwork]}'
  3. Inventory existing firewall rules affecting worker nodes

    • List all firewall rules in the relevant VPC network(s):
      gcloud compute networks list --project PROJECT_ID \
      --format="table(name,autoCreateSubnetworks)"

      gcloud compute firewall-rules list \
      --project PROJECT_ID \
      --format="table(name,network,direction,priority,sourceRanges,targetTags,targetServiceAccounts,allowed,denied)"
    • For each worker node tag or service account from step 2, filter rules that apply:
      gcloud compute firewall-rules list \
      --project PROJECT_ID \
      --filter="targetTags:list(TAG1,TAG2)" \
      --format="table(name,direction,sourceRanges,allowed,denied)"

      gcloud compute firewall-rules list \
      --project PROJECT_ID \
      --filter="targetServiceAccounts:SA_EMAIL" \
      --format="table(name,direction,sourceRanges,allowed,denied)"
  4. Decide the minimal required traffic

    • Review which sources legitimately need to reach worker nodes (e.g., control-plane IPs, load balancer health checks, CI/CD CIDRs, on-prem CIDRs) and which egress from nodes is required (e.g., Google APIs, container registries, specific partner services).
    • Document allowed CIDR ranges, ports/protocols, and whether scoping by target tags or target service accounts is preferable for each node pool.
  5. Create or tighten firewall rules based on the decision

    • For each required traffic pattern, create least-privilege rules scoped by tags or service accounts (example template; replace placeholders):
      gcloud compute firewall-rules create WORKER-INGRESS-ALLOWED \
      --project PROJECT_ID \
      --network VPC_NETWORK_NAME \
      --priority 1000 \
      --direction INGRESS \
      --action ALLOW \
      --target-tags WORKER_NODE_TAG \
      --target-service-accounts WORKER_SA_EMAIL \
      --source-ranges SOURCE_CIDR_1,SOURCE_CIDR_2 \
      --rules tcp:443,tcp:10250

      gcloud compute firewall-rules create WORKER-EGRESS-ALLOWED \
      --project PROJECT_ID \
      --network VPC_NETWORK_NAME \
      --priority 1000 \
      --direction EGRESS \
      --action ALLOW \
      --target-tags WORKER_NODE_TAG \
      --target-service-accounts WORKER_SA_EMAIL \
      --destination-ranges DEST_CIDR_1,DEST_CIDR_2 \
      --rules tcp:443
    • Remove or lower-priority broad rules that unnecessarily allow 0.0.0.0/0 or wide port ranges to worker node tags/service accounts, ensuring you do not block required control-plane or health-check traffic.
  6. Verify and re-assess exposure

    • Re-list firewall rules and confirm workers are only covered by appropriately scoped rules:
      gcloud compute firewall-rules list \
      --project PROJECT_ID \
      --filter="targetTags:list(WORKER_NODE_TAG) OR targetServiceAccounts:WORKER_SA_EMAIL" \
      --format="table(name,direction,sourceRanges,destinationRanges,allowed,denied,priority)"
    • Optionally, perform connectivity tests from expected sources and from disallowed networks to confirm access is correctly restricted.
Using kubectl

kubectl cannot configure Google Cloud firewall rules or VPC networking; those changes must be made in the GCP console, via gcloud, or through IaC (e.g., Terraform) at the cloud provider level. Refer to the Manual Steps section for guidance on reviewing and implementing appropriate firewall rules for GKE worker nodes.

Automation
#!/usr/bin/env bash
set -euo pipefail

# REQUIREMENTS:
# - Runs on: any machine with gcloud, kubectl, and jq configured
# - Auth: gcloud auth logged in with permissions to list GKE clusters, nodes, and firewall rules

PROJECT_ID="my-project-id" # <-- set explicitly
REGION="" # optional: set to restrict to a region, else all regions in project

################################################################################
# Helper: list GKE clusters (optionally by region)
################################################################################
if [[ -n "${REGION}" ]]; then
CLUSTERS_JSON=$(gcloud container clusters list \
--project "${PROJECT_ID}" \
--region "${REGION}" \
--format=json)
else
CLUSTERS_JSON=$(gcloud container clusters list \
--project "${PROJECT_ID}" \
--format=json)
fi

echo "===== GKE Node Firewall Review Report (project: ${PROJECT_ID}) ====="
echo

echo "${CLUSTERS_JSON}" | jq -r '.[] | [.name, .location, .network, .subnetwork] | @tsv' | \
while IFS=$'\t' read -r CLUSTER LOCATION NETWORK SUBNETWORK; do
echo "-------------------------------------------------------------------------------"
echo "Cluster: ${CLUSTER} Location: ${LOCATION}"
echo "VPC Network: ${NETWORK}"
echo "Subnetwork: ${SUBNETWORK}"
echo

# Configure kubectl for this cluster
gcloud container clusters get-credentials "${CLUSTER}" \
--project "${PROJECT_ID}" \
--location "${LOCATION}" >/dev/null 2>&1

##############################################################################
# 1. List node instances, their tags, service accounts, and networks
##############################################################################
echo "[1] Node instances, tags, service accounts, and networks"
NODE_NAMES=$(kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{" "}{end}')

if [[ -z "${NODE_NAMES}" ]]; then
echo " No nodes found in cluster."
echo
continue
fi

for NODE in ${NODE_NAMES}; do
# Get underlying GCE instance name and zone from node labels
INSTANCE_NAME=$(kubectl get node "${NODE}" -o jsonpath='{.metadata.name}')
INSTANCE_ZONE=$(kubectl get node "${NODE}" \
-o jsonpath='{.metadata.labels."topology\.gke\.io/zone"}')

if [[ -z "${INSTANCE_ZONE}" ]]; then
# Fallback to old label
INSTANCE_ZONE=$(kubectl get node "${NODE}" \
-o jsonpath='{.metadata.labels."topology\.kubernetes\.io/zone"}')
fi

echo " Node: ${NODE}"
echo " GCE instance: ${INSTANCE_NAME}"
echo " Zone: ${INSTANCE_ZONE}"

INSTANCE_JSON=$(gcloud compute instances describe "${INSTANCE_NAME}" \
--zone "${INSTANCE_ZONE}" \
--project "${PROJECT_ID}" \
--format=json)

echo "${INSTANCE_JSON}" | jq -r '
" Tags: " + ( .tags.items // [] | join(",") | if . == "" then "(none)" else . end ) + "\n" +
" Service Accounts: " + ( .serviceAccounts[].email // "" ) + "\n" +
" Network(s): " + ( .networkInterfaces[].network // "" ) + "\n" +
" Subnetwork(s): " + ( .networkInterfaces[].subnetwork // "" )
'
echo
done

##############################################################################
# 2. List firewall rules that could apply to these node instances
##############################################################################
echo "[2] Firewall rules potentially applying to cluster nodes"
echo " (filtered by network: ${NETWORK})"
echo

# Get all unique tags and service accounts for nodes in this cluster
TMP_NODE_INFO=$(mktemp)
for NODE in ${NODE_NAMES}; do
INSTANCE_NAME=$(kubectl get node "${NODE}" -o jsonpath='{.metadata.name}')
INSTANCE_ZONE=$(kubectl get node "${NODE}" \
-o jsonpath='{.metadata.labels."topology\.gke\.io/zone"}')
if [[ -z "${INSTANCE_ZONE}" ]]; then
INSTANCE_ZONE=$(kubectl get node "${NODE}" \
-o jsonpath='{.metadata.labels."topology\.kubernetes\.io/zone"}')
fi

gcloud compute instances describe "${INSTANCE_NAME}" \
--zone "${INSTANCE_ZONE}" \
--project "${PROJECT_ID}" \
--format=json >> "${TMP_NODE_INFO}"
done

NODE_TAGS=$(jq -r '.tags.items[]? // empty' "${TMP_NODE_INFO}" | sort -u)
NODE_SAS=$(jq -r '.serviceAccounts[].email? // empty' "${TMP_NODE_INFO}" | sort -u)

echo " Node tags:"
if [[ -n "${NODE_TAGS}" ]]; then
echo "${NODE_TAGS}" | sed 's/^/ - /'
else
echo " - (none)"
fi

echo " Node service accounts:"
if [[ -n "${NODE_SAS}" ]]; then
echo "${NODE_SAS}" | sed 's/^/ - /'
else
echo " - (none)"
fi
echo

# List all firewall rules on this network
FW_JSON=$(gcloud compute firewall-rules list \
--project "${PROJECT_ID}" \
--filter="network:${NETWORK}" \
--format=json)

echo " Firewall rules on network ${NETWORK}:"
echo "${FW_JSON}" | jq -r '
.[] |
" - Name: " + .name + "\n" +
" Direction: " + .direction + "\n" +
" Priority: " + ( .priority | tostring ) + "\n" +
" Action: " + .allowed[0].IPProtocol? // .denied[0].IPProtocol? // "N/A" + "\n" +
" Disabled: " + ( .disabled | tostring ) + "\n" +
" Target tags: " + ( .targetTags // [] | join(",") ) + "\n" +
" Target service accounts: " + ( .targetServiceAccounts // [] | join(",") ) + "\n" +
" Source ranges: " + ( .sourceRanges // [] | join(",") ) + "\n" +
" Source tags: " + ( .sourceTags // [] | join(",") ) + "\n" +
" Source service accounts: " + ( .sourceServiceAccounts // [] | join(",") ) + "\n" +
" Destination ranges: " + ( .destinationRanges // [] | join(",") ) + "\n" +
" Allowed: " + ( .allowed // [] | map(.IPProtocol + ":" + ((.ports // []) | join(","))) | join(";") ) + "\n" +
" Denied: " + ( .denied // [] | map(.IPProtocol + ":" + ((.ports // []) | join(","))) | join(";") ) + "\n"
'

rm -f "${TMP_NODE_INFO}"
echo
done

echo "===== END OF REPORT ====="

How to interpret the output (what indicates a problem)

Review the report for each cluster and flag cases like:

  • Nodes with tags or service accounts that are not used by any restrictive firewall rules (no targetTags / targetServiceAccounts match), meaning they rely only on wide-open network rules.
  • Firewall rules with:
    • Very broad sourceRanges (e.g. 0.0.0.0/0 or full private ranges) reaching node tags/service accounts.
    • No targetTags or targetServiceAccounts (applies to all instances in the network).
    • Allowed protocols/ports that are unnecessary or very broad (e.g. tcp:1-65535, all).
  • Absence of dedicated rules that:
    • Limit inbound access to nodes only from known admin/source CIDRs or specific service accounts/tags.
    • Limit east‑west (lateral) traffic between node pools, environments, or workloads where isolation is desired.

Use these findings to manually design tighter firewall rules with gcloud compute firewall-rules create as per the benchmark remediation.