More Info:
Reduce The Network Attack Surface Of Gke Nodes By Using Firewalls To Restrict Ingress And Egress TrafficRisk Level
MediumAddress
SecurityCompliance Standards
- CIS GKE
Triage and Remediation
- Remediation
Remediation
Manual Steps
Manual Steps
-
Identify node networks, tags, and service accounts
Run on any machine with gcloud access:Review which VPC/subnets your nodes use, their network tags, and node service accounts.PROJECT_ID="<PROJECT_ID>" CLUSTER_NAME="<GKE_CLUSTER_NAME>" LOCATION="<CLUSTER_LOCATION>" # e.g. us-central1-b or us-central1 gcloud container clusters describe "$CLUSTER_NAME" \ --zone "$LOCATION" \ --project "$PROJECT_ID" \ --format="json(network,subnetwork,nodePools)" gcloud compute instances list \ --project "$PROJECT_ID" \ --filter="metadata.cluster-name=$CLUSTER_NAME" \ --format="json(name,zone,tags,serviceAccounts,networkInterfaces)" -
List current firewall rules affecting node networks/tags
NETWORK_SELF_LINK="<NETWORK_SELF_LINK_FROM_STEP_1>" gcloud compute firewall-rules list \ --project "$PROJECT_ID" \ --filter="network=$NETWORK_SELF_LINK" \ --format="json(name,network,direction,priority,allowed,denied,\
Identify rules whose `targetTags` or `targetServiceAccounts` match the nodes, and note overly broad `0.0.0.0/0` or large CIDR ranges and wide `allowed` ports (0-65535, tcp:*, udp:*).
3. **Determine required ingress/egress for worker nodes**
Using GKE documentation and your application requirements, list:
- Required **ingress** sources: control plane, load balancers, admins, other services.
- Required **egress** destinations: control plane, container registry, logging/monitoring, DNS, specific external services.
Decide which traffic can be narrowed by:
- Restricting CIDR ranges
- Restricting ports/protocols
- Using `target-tags` / `target-service-accounts` instead of broad targets.
4. **Design and (optionally) test restrictive firewall rules**
For each needed traffic pattern, define a corresponding rule. Example (ingress from control plane to nodes on required ports only):
```bash
gcloud compute firewall-rules create "gke-nodes-ingress-controlplane" \
--project "$PROJECT_ID" \
--network "$NETWORK_SELF_LINK" \
--priority 1000 \
--direction INGRESS \
--action ALLOW \
--target-tags "gke-node" \
--source-ranges "<CONTROL_PLANE_CIDR>" \
--rules "tcp:10250,tcp:30000-32767"
gcloud compute firewall-rules create "gke-nodes-egress-restricted" \
--project "$PROJECT_ID" \
--network "$NETWORK_SELF_LINK" \
--priority 1000 \
--direction EGRESS \
--action ALLOW \
--target-tags "gke-node" \
--destination-ranges "<ALLOWED_DEST_CIDR>" \
--rules "tcp:443,udp:53"
-
Tighten or remove overly permissive rules
For rules you deem too broad:- Edit them in your IaC (if applicable) and redeploy, or:
- Update or delete directly:
or
gcloud compute firewall-rules update "<RULE_NAME>" \ --project "$PROJECT_ID" \ --source-ranges "<NARROWED_CIDR_RANGES>" \ --rules "<NARROWED_RULES>"gcloud compute firewall-rules delete "<RULE_NAME>" \ --project "$PROJECT_ID"
-
Verify node exposure after changes
Re-run:Confirm that:gcloud compute firewall-rules list \ --project "$PROJECT_ID" \ --filter="network=$NETWORK_SELF_LINK" \ --format="table(name,direction,priority,sourceRanges,destinationRanges,allowed,denied,targetTags,targetServiceAccounts)"- Worker-node-related rules have specific
target-tags/target-service-accounts. - Ingress and egress rules use the minimal necessary CIDR ranges and ports.
- No remaining broad
0.0.0.0/0rules apply to worker nodes unless explicitly justified.
- Worker-node-related rules have specific
Using kubectl
Using kubectl
kubectl cannot configure GKE node firewall rules, because they are managed at the Google Cloud VPC / firewall level, not via Kubernetes API objects. Configure firewall rules using Google Cloud (console,
gcloud, or IaC) as described in the Manual Steps section.Automation
Automation
#!/usr/bin/env bash
# Purpose:
# Summarize GKE node networks / tags / service accounts and the
# firewall rules that apply to them, so you can manually review
# whether worker nodes are sufficiently firewalled.
#
# Requirements:
# - Run on any machine with:
# * gcloud configured (project, auth)
# * kubectl configured for the target cluster
# * jq installed
set -euo pipefail
PROJECT_ID="$(gcloud config get-value project 2>/dev/null)"
if [[ -z "${PROJECT_ID}" ]]; then
echo "ERROR: gcloud project not set. Run: gcloud config set project YOUR_PROJECT_ID" >&2
exit 1
fi
echo "Using project: ${PROJECT_ID}" >&2
echo >&2
# 1. Discover node pools / nodes from the cluster via kubectl
echo "=== Kubernetes nodes (from kubectl) ===" >&2
kubectl get nodes -o wide
echo >&2
NODE_INSTANCE_NAMES=($(kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{" "}{end}'))
if [[ ${#NODE_INSTANCE_NAMES[@]} -eq 0 ]]; then
echo "No nodes found via kubectl; exiting." >&2
exit 0
fi
# 2. Map Kubernetes node names to GCE instance selfLinks
echo "=== Mapping Kubernetes nodes to GCE instances ===" >&2
kubectl get nodes -o json | jq -r '.items[] |
{name: .metadata.name, providerID: .spec.providerID} |
.name + " " + (.providerID // "")' | sed 's|gce://||'
echo >&2
# Extract instance names and zones from providerID, if present
GCE_INSTANCES=()
GCE_ZONES=()
while read -r k8s_name provider_id; do
# provider_id format: projects/PROJECT_ID/zones/ZONE/instances/INSTANCE_NAME
if [[ -z "${provider_id}" ]]; then
continue
fi
zone="$(awk -F'/' '{print $(NF-3)}' <<< "${provider_id}")"
inst="$(awk -F'/' '{print $NF}' <<< "${provider_id}")"
GCE_INSTANCES+=("${inst}")
GCE_ZONES+=("${zone}")
done < <(kubectl get nodes -o json | jq -r '.items[] |
{name: .metadata.name, providerID: .spec.providerID} |
.name + " " + (.providerID // "")' | sed 's|gce://||')
if [[ ${#GCE_INSTANCES[@]} -eq 0 ]]; then
echo "No GCE-backed nodes discovered from providerID; ensure this is a GKE cluster." >&2
exit 0
fi
# Deduplicate zone list
mapfile -t UNIQUE_ZONES < <(printf "%s\n" "${GCE_ZONES[@]}" | sort -u)
echo "Discovered zones for nodes: ${UNIQUE_ZONES[*]}" >&2
echo >&2
# 3. Gather firewall-relevant properties for each node instance
echo "=== Node instance firewall-relevant properties ==="
for zone in "${UNIQUE_ZONES[@]}"; do
echo
echo "Zone: ${zone}"
gcloud compute instances list \
--project "${PROJECT_ID}" \
--zones "${zone}" \
--format 'table(
name,
zone.basename(),
networkInterfaces[0].network.scope(),
networkInterfaces[0].subnetwork.scope(),
networkInterfaces[0].networkIP,
tags.items.list():label=tags,
serviceAccounts[].email.list():label=serviceAccounts
)'
done
echo
# 4. List firewall rules that may apply to these nodes
echo "=== Firewall rules that may apply to worker nodes ==="
# Get all unique tags and service accounts from the instances we found
NODE_TAGS=()
NODE_SAS=()
for zone in "${UNIQUE_ZONES[@]}"; do
while IFS= read -r tag; do
[[ -n "${tag}" ]] && NODE_TAGS+=("${tag}")
done < <(gcloud compute instances list \
--project "${PROJECT_ID}" \
--zones "${zone}" \
--filter="name=(${GCE_INSTANCES[*]})" \
--format='flattened(tags.items[])' 2>/dev/null | awk '{print $2}' | sed '/^$/d')
while IFS= read -r sa; do
[[ -n "${sa}" ]] && NODE_SAS+=("${sa}")
done < <(gcloud compute instances list \
--project "${PROJECT_ID}" \
--zones "${zone}" \
--filter="name=(${GCE_INSTANCES[*]})" \
--format='flattened(serviceAccounts.email)' 2>/dev/null | awk '{print $2}' | sed '/^$/d')
done
mapfile -t UNIQUE_TAGS < <(printf "%s\n" "${NODE_TAGS[@]:-}" | sort -u)
mapfile -t UNIQUE_SAS < <(printf "%s\n" "${NODE_SAS[@]:-}" | sort -u)
echo "Worker node tags: ${UNIQUE_TAGS[*]:-(none)}"
echo "Worker node service accounts: ${UNIQUE_SAS[*]:-(none)}"
echo
echo "--- Relevant firewall rules (by targetTags) ---"
if [[ ${#UNIQUE_TAGS[@]} -gt 0 ]]; then
gcloud compute firewall-rules list \
--project "${PROJECT_ID}" \
--filter="($(printf 'targetTags:%s OR ' "${UNIQUE_TAGS[@]}" | sed 's/ OR $//'))" \
--format='table(
name,
direction,
priority,
disabled,
network,
targetTags.list(),
targetServiceAccounts.list(),
sourceRanges.list(),
destinationRanges.list(),
allowed[].map().firewall_rule().list():label=allowed,
denied[].map().firewall_rule().list():label=denied
)'
else
echo "No tags found on worker nodes."
fi
echo
echo "--- Relevant firewall rules (by targetServiceAccounts) ---"
if [[ ${#UNIQUE_SAS[@]} -gt 0 ]]; then
gcloud compute firewall-rules list \
--project "${PROJECT_ID}" \
--filter="($(printf 'targetServiceAccounts:%s OR ' "${UNIQUE_SAS[@]}" | sed 's/ OR $//'))" \
--format='table(
name,
direction,
priority,
disabled,
network,
targetTags.list(),
targetServiceAccounts.list(),
sourceRanges.list(),
destinationRanges.list(),
allowed[].map().firewall_rule().list():label=allowed,
denied[].map().firewall_rule().list():label=denied
)'
else
echo "No service accounts found on worker nodes (unexpected for GKE)."
fi
echo
echo "=== Guidance: what output indicates a problem? ==="
cat <<'EOF'
Review the above data looking for:
1) Nodes with no specific firewall selectors
- Instances show empty 'tags' AND use generic / shared service accounts.
- Effect: firewall rules likely apply broadly to the whole network/subnet, not tightly scoped to worker nodes.
2) Overly permissive ingress
- Firewall rules with:
direction: INGRESS
sourceRanges: 0.0.0.0/0 (or very broad CIDRs)
combined with:
allowed: tcp/udp/icmp "all" or many ports
and targeting worker node tags or service accounts.
- This indicates a large attack surface for inbound traffic to nodes.
3) Overly permissive egress
- Firewall rules with:
direction: EGRESS
destinationRanges: 0.0.0.0/0
and allowing many or all protocols/ports without restriction.
- This indicates lack of outbound control from worker nodes.
4) Missing explicit rules
- If you see that nodes share the same network and subnet as many other workloads,
but there are few or no firewall rules targeting their specific tags/service accounts,
then worker node traffic is likely governed only by coarse, shared rules.
5) Disabled or shadowed rules
- disabled: true on rules that were intended to restrict node traffic.
- Very low priority "allow" rules that override more restrictive ones.
Any of the above patterns suggest that the cluster does not adequately "firewall GKE worker nodes"
as recommended, and you should tighten or redesign firewall rules using:
gcloud compute firewall-rules create/patch/delete ...
EOF

