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
Remediation
Manual Steps
-
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/endpointPrivateAccesssettings.publicAccessCidrs.- Whether worker nodes (in your nodegroups/Auto Scaling Groups) are launched in public subnets with public IPs (see next step).
- On any machine with AWS CLI access:
-
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.
- List nodegroups:
-
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.endpointPublicAccessset tofalseortruewith tightly scopedpublicAccessCidrs.
- Determine:
-
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'
- If you must keep some public endpoint access but restrict it:
-
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.
- In the AWS console or your IaC, for each nodegroup:
-
Verify that nodes are private and exposure is minimized (CLI)
- On any machine with kubectl access:
Confirm node INTERNAL-IP values are RFC1918/private ranges and no EXTERNAL-IP is assigned.kubectl get nodes -o wide
- From AWS CLI, confirm worker instances have no public IPs:
Ensure the output isaws ec2 describe-instances \--region us-east-1 \--filters "Name=tag:eks:cluster-name,Values=my-cluster" \--query 'Reservations[].Instances[].PublicIpAddress'
nullfor all instances, and thatdescribe-clusterstill shows the intendedresourcesVpcConfig.
- On any machine with kubectl access:
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