Skip to main content

Consider Firewalling Gke Worker Nodes

More Info:

Reduce The Network Attack Surface Of Gke Nodes By Using Firewalls To Restrict Ingress And Egress Traffic

Risk Level

Medium

Address

Security

Compliance Standards

  • CIS GKE

Triage and Remediation

Remediation

Manual Steps
  1. Identify node networks, tags, and service accounts
    Run on any machine with gcloud access:

    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)"

    Review which VPC/subnets your nodes use, their network tags, and node service accounts.

  2. 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,\

sourceRanges,sourceTags,sourceServiceAccounts,targetTags,targetServiceAccounts)"

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"

For egress restriction example:

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"

Adjust names, CIDRs, tags, ports, and directions according to your design.

  1. 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:
      gcloud compute firewall-rules update "<RULE_NAME>" \
      --project "$PROJECT_ID" \
      --source-ranges "<NARROWED_CIDR_RANGES>" \
      --rules "<NARROWED_RULES>"
      or
      gcloud compute firewall-rules delete "<RULE_NAME>" \
      --project "$PROJECT_ID"

    Make changes incrementally and monitor workload behavior to avoid breaking connectivity.

  2. Verify node exposure after changes
    Re-run:

    gcloud compute firewall-rules list \
    --project "$PROJECT_ID" \
    --filter="network=$NETWORK_SELF_LINK" \
    --format="table(name,direction,priority,sourceRanges,destinationRanges,allowed,denied,targetTags,targetServiceAccounts)"

    Confirm that:

    • 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/0 rules apply to worker nodes unless explicitly justified.
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
#!/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

Additional Reading: