Skip to main content

Ensure Clusters Are Created With Private Nodes

More Info:

Provision worker nodes without public IP addresses (private nodes) so that node network exposure to the internet is minimized.

Risk Level

High

Address

Security

Compliance Standards

  • CIS EKS

Triage and Remediation

Remediation

Manual Steps
  1. Review current endpoint and node IP configuration (CLI)

    • On any machine with AWS CLI access:
      aws eks describe-cluster \
      --region us-east-1 \
      --name my-cluster \
      --query 'cluster.resourcesVpcConfig'
    • Note:
      • endpointPublicAccess / endpointPrivateAccess settings.
      • publicAccessCidrs.
      • Whether worker nodes (in your nodegroups/Auto Scaling Groups) are launched in public subnets with public IPs (see next step).
  2. Inspect nodegroup networking and IP assignment (CLI)

    • List nodegroups:
      aws eks list-nodegroups \
      --region us-east-1 \
      --cluster-name my-cluster
    • For each nodegroup:
      aws eks describe-nodegroup \
      --region us-east-1 \
      --cluster-name my-cluster \
      --nodegroup-name <NODEGROUP_NAME> \
      --query 'nodegroup.subnets'
    • For each subnet ID returned, check if it is public (has a route to an Internet Gateway) and whether instances launched there get public IPs:
      aws ec2 describe-subnets \
      --region us-east-1 \
      --subnet-ids <SUBNET_ID>
    • In the console, confirm whether “Auto-assign public IPv4 address” is enabled for these subnets or on the Launch Template used by the nodegroup.
  3. Decide target posture: private nodes with restricted API endpoint

    • Determine:
      • Which subnets will host worker nodes (should be private subnets with NAT access, no Internet Gateway route).
      • Which IP ranges (e.g., your bastion or office IPs) require access to the public EKS endpoint, if any.
    • Document desired:
      • Nodegroups only in private subnets without public IP assignment.
      • endpointPrivateAccess=true.
      • endpointPublicAccess set to false or true with tightly scoped publicAccessCidrs.
  4. Reconfigure cluster endpoint access (CLI or console)

    • If you must keep some public endpoint access but restrict it:
      aws eks update-cluster-config \
      --region us-east-1 \
      --name my-cluster \
      --resources-vpc-config \
      endpointPublicAccess=true,endpointPrivateAccess=true,publicAccessCidrs="203.0.113.5/32"
    • If you can rely only on private access (ensure network connectivity and tooling first):
      aws eks update-cluster-config \
      --region us-east-1 \
      --name my-cluster \
      --resources-vpc-config \
      endpointPublicAccess=false,endpointPrivateAccess=true
    • Validate change:
      aws eks describe-cluster \
      --region us-east-1 \
      --name my-cluster \
      --query 'cluster.resourcesVpcConfig'
  5. Migrate or create nodegroups without public IPs (console or IaC)

    • In the AWS console or your IaC, for each nodegroup:
      • Ensure it uses only private subnets (no direct route to an Internet Gateway).
      • Ensure instances do not auto-assign public IPv4 addresses (via subnet setting or Launch Template network interface configuration).
    • If current nodegroups use public subnets or public IPs, create replacement nodegroups in private subnets, cordon/drain old nodes, then delete the old nodegroups.
  6. Verify that nodes are private and exposure is minimized (CLI)

    • On any machine with kubectl access:
      kubectl get nodes -o wide
      Confirm node INTERNAL-IP values are RFC1918/private ranges and no EXTERNAL-IP is assigned.
    • From AWS CLI, confirm worker instances have no public IPs:
      aws ec2 describe-instances \
      --region us-east-1 \
      --filters "Name=tag:eks:cluster-name,Values=my-cluster" \
      --query 'Reservations[].Instances[].PublicIpAddress'
      Ensure the output is null for all instances, and that describe-cluster still shows the intended resourcesVpcConfig.
Using kubectl

kubectl cannot be used to create or convert EKS worker nodes to private nodes, because this setting is managed at the AWS control-plane / VPC configuration layer (console, CLI, or IaC), not via Kubernetes API objects. Use the cloud provider configuration as described in the Manual Steps section to review and adjust your cluster’s networking and node exposure.

Automation
#!/usr/bin/env bash
#
# Report EKS cluster API endpoint exposure and node public IP usage.
# Requirements:
# - aws CLI configured with at least read-only EKS/EC2/VPC permissions
# - kubectl configured (used only to verify the cluster is reachable)
#
# Usage:
# ./eks_private_nodes_report.sh <region> <cluster-name>
#
# Run from: any machine with aws + kubectl access

set -euo pipefail

REGION="${1:-}"
CLUSTER_NAME="${2:-}"

if [[ -z "$REGION" || -z "$CLUSTER_NAME" ]]; then
echo "Usage: $0 <region> <cluster-name>" >&2
exit 1
fi

echo "=== Verifying kubectl access to cluster '${CLUSTER_NAME}' in region '${REGION}' ==="
aws eks update-kubeconfig --region "$REGION" --name "$CLUSTER_NAME" >/dev/null
kubectl get nodes -o wide || {
echo "ERROR: Unable to reach cluster with kubectl." >&2
exit 1
}

echo
echo "=== EKS Cluster Endpoint Configuration (control plane) ==="
aws eks describe-cluster \
--region "$REGION" \
--name "$CLUSTER_NAME" \
--query 'cluster.resourcesVpcConfig.{endpointPublicAccess:endpointPublicAccess,endpointPrivateAccess:endpointPrivateAccess,publicAccessCidrs:publicAccessCidrs}' \
--output table

echo
echo "=== Interpretation (control plane) ==="
cat <<'EOF'
- endpointPublicAccess = true AND publicAccessCidrs includes 0.0.0.0/0
-> RISK: API server is exposed to the entire internet.
- endpointPublicAccess = true AND publicAccessCidrs is a broad range (e.g. /0, /8, /16)
-> RISK: API server is exposed to large address ranges.
- endpointPrivateAccess = false
-> RISK: No private endpoint; all access must go through the public endpoint.

Recommended pattern (per control intent):
- endpointPrivateAccess = true
- endpointPublicAccess = false
OR
- endpointPublicAccess = true with tightly scoped publicAccessCidrs to specific admin IPs/VPN.
EOF

echo
echo "=== Nodegroup Public IP Configuration (whether nodes may get public IPs) ==="
aws eks list-nodegroups \
--region "$REGION" \
--cluster-name "$CLUSTER_NAME" \
--query 'nodegroups' \
--output text | tr '\t' '\n' | while read -r NG; do
[[ -z "$NG" ]] && continue
echo
echo "Nodegroup: $NG"
aws eks describe-nodegroup \
--region "$REGION" \
--cluster-name "$CLUSTER_NAME" \
--nodegroup-name "$NG" \
--query 'nodegroup.{subnets:subnets,remoteAccess:remoteAccess,launchTemplate:launchTemplate}' \
--output json

echo " -> Auto-assign public IP on subnets (indicates potential for public nodes):"
SUBNETS=$(aws eks describe-nodegroup \
--region "$REGION" \
--cluster-name "$CLUSTER_NAME" \
--nodegroup-name "$NG" \
--query 'nodegroup.subnets' \
--output text | tr '\t' '\n')

for SN in $SUBNETS; do
MAP_PUBLIC_IP=$(aws ec2 describe-subnets \
--region "$REGION" \
--subnet-ids "$SN" \
--query 'Subnets[0].MapPublicIpOnLaunch' \
--output text)
echo " Subnet $SN: MapPublicIpOnLaunch=${MAP_PUBLIC_IP}"
done
done

echo
echo "=== Interpretation (nodegroups/subnets) ==="
cat <<'EOF'
- For any subnet with MapPublicIpOnLaunch = true:
-> RISK: EC2 instances (including worker nodes) in this subnet will receive public IPs
unless overridden by the launch template or other settings.
- To meet the intent of "private nodes":
-> Worker nodes should be in subnets with MapPublicIpOnLaunch = false (private subnets),
behind NAT gateways or similar, with NO direct public IPs assigned.

You must review:
- Whether any nodegroup subnets have MapPublicIpOnLaunch = true.
- Whether that configuration is acceptable for your risk profile.
EOF

echo
echo "=== (Optional) Live Node Public IP Check via EC2 ==="
echo "Gathering node instance IDs from cluster..."
NODE_INSTANCE_IDS=$(kubectl get nodes -o jsonpath='{range .items[*]}{.spec.providerID}{"\n"}{end}' \
| sed -n 's|^aws:///[^/]*/||p' | sort -u)

if [[ -z "$NODE_INSTANCE_IDS" ]]; then
echo "No node instance IDs found from kubectl; skipping EC2 IP check."
exit 0
fi

echo "Checking public IPs for current worker nodes..."
aws ec2 describe-instances \
--region "$REGION" \
--instance-ids $NODE_INSTANCE_IDS \
--query 'Reservations[].Instances[].{InstanceId:InstanceId,PrivateIpAddress:PrivateIpAddress,PublicIpAddress:PublicIpAddress,SubnetId:SubnetId}' \
--output table

cat <<'EOF'

Interpretation (live nodes):
- Any node with a non-empty PublicIpAddress
-> RISK: That worker node is directly reachable from the internet (subject to security groups).
- For strict "private nodes":
-> All worker nodes should show PublicIpAddress as blank/null.

This script only reports state. Decisions and remediation (changing endpoint access,
moving nodegroups to private subnets, adjusting subnet attributes, or recreating
nodegroups) must be made manually based on your security requirements.
EOF