> ## Documentation Index
> Fetch the complete documentation index at: https://cloudanix.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Ensure Clusters Are Created With Private Nodes

### More Info:

Disable public IP addresses for cluster nodes, so that they only have private IP addresses. Private Nodes are nodes with no public IP addresses.

### Risk Level

Low

### Address

Security

### Compliance Standards

* APRA CPS 234 (Australia)
* AWS Startup Security Baseline
* BSI C5 (Germany)
* Brazil LGPD
* CCPA / CPRA (California)
* CIS Critical Security Controls v8
* CIS EKS
* CMMC 2.0
* CSA Cloud Controls Matrix v4
* DPDPA
* Digital Operational Resilience Act (EU)
* ISO/IEC 27017
* ISO/IEC 27018
* ISO/IEC 27701
* KSA PDPL
* MAS Technology Risk Management (Singapore)
* MITRE ATT\&CK (Cloud)
* NIS2 Directive
* NIST SP 800-171
* NYDFS 23 NYCRR 500
* SWIFT Customer Security Controls Framework
* Sarbanes-Oxley IT General Controls
* UK NCSC Cyber Assessment Framework

### Triage and Remediation

<Tabs>
  <Tab title="Remediation">
    ### Remediation

    <AccordionGroup>
      <Accordion title="Manual Steps" defaultOpen="true">
        1. **Review current EKS cluster endpoint and node networking configuration**
           * Run on any machine with AWS CLI access:
             ```bash theme={null}
             aws eks describe-cluster \
               --region us-east-1 \
               --name my-cluster \
               --output json
             ```
           * Inspect `.cluster.resourcesVpcConfig`:
             * `endpointPublicAccess`
             * `publicAccessCidrs`
             * `endpointPrivateAccess`
             * `subnetIds` (to verify they are private subnets for nodes)

        2. **Confirm whether worker nodes are using public IPs**
           * Get the nodegroup names:
             ```bash theme={null}
             aws eks list-nodegroups \
               --region us-east-1 \
               --cluster-name my-cluster
             ```
           * For each nodegroup:
             ```bash theme={null}
             aws eks describe-nodegroup \
               --region us-east-1 \
               --cluster-name my-cluster \
               --nodegroup-name <NODEGROUP_NAME> \
               --output json
             ```
           * Check `.resourcesVpcConfig.subnets` and then inspect those subnets:
             ```bash theme={null}
             aws ec2 describe-subnets \
               --region us-east-1 \
               --subnet-ids <SUBNET_ID_1> <SUBNET_ID_2> ...
             ```
           * Confirm they are private subnets (no direct route to an Internet Gateway) and that your nodegroups are not configured with public IP assignment (check `ec2:AssociatePublicIpAddress` in Launch Template or nodegroup config via the console/IaC).

        3. **Decide on the desired access model for the control plane endpoint**
           * If the cluster should be reachable only from inside the VPC or via VPN/Direct Connect, plan to:
             * Set `endpointPrivateAccess=true`
             * Set `endpointPublicAccess=false`
           * If you must keep public endpoint access temporarily, define the minimal `publicAccessCidrs` necessary (e.g., specific office/VPN egress IPs).
           * Ensure alternative access (bastion, VPN, Direct Connect) is in place before removing broad public access so operational access is not lost.

        4. **Update the cluster endpoint access configuration (if changes are needed)**
           * To enable private access and restrict public access to specific CIDRs (example):
             ```bash theme={null}
             aws eks update-cluster-config \
               --region us-east-1 \
               --name my-cluster \
               --resources-vpc-config \
                 endpointPublicAccess=true,publicAccessCidrs="203.0.113.5/32",endpointPrivateAccess=true
             ```
           * To disable public access entirely and use only private access:
             ```bash theme={null}
             aws eks update-cluster-config \
               --region us-east-1 \
               --name my-cluster \
               --resources-vpc-config \
                 endpointPublicAccess=false,endpointPrivateAccess=true
             ```
           * Wait for the update to complete:
             ```bash theme={null}
             aws eks describe-cluster \
               --region us-east-1 \
               --name my-cluster \
               --query 'cluster.status'
             ```

        5. **Ensure all nodegroups use only private IPs**
           * For managed nodegroups:
             * Reconfigure or recreate nodegroups so that:
               * They use only private subnets.
               * Public IP assignment is disabled in the nodegroup or its Launch Template (via AWS console or your IaC).
           * For each updated or recreated nodegroup, verify that new EC2 instances have no public IP:
             ```bash theme={null}
             aws ec2 describe-instances \
               --region us-east-1 \
               --filters "Name>tag:eks:cluster-name,Values=my-cluster" \
               --query 'Reservations[].Instances[].{Id:InstanceId,PublicIp:PublicIpAddress,PrivateIp:PrivateIpAddress}' \
               --output table
             ```
           * Confirm `PublicIp` is empty for all instances.

        6. **Verify the final state against the benchmark intent**
           * Confirm cluster endpoint config:
             ```bash theme={null}
             aws eks describe-cluster \
               --region us-east-1 \
               --name my-cluster \
               --query 'cluster.resourcesVpcConfig.{endpointPublicAccess:endpointPublicAccess,publicAccessCidrs:publicAccessCidrs,endpointPrivateAccess:endpointPrivateAccess}' \
               --output json
             ```
           * Confirm worker nodes have only private IPs (re-run instance check from step 5).
           * Document any justified exceptions where limited public access (`endpointPublicAccess=true` with tightly scoped `publicAccessCidrs`) is required for business or operational reasons.
      </Accordion>

      <Accordion title="Using kubectl">
        kubectl cannot modify whether an EKS cluster uses private nodes or adjust its VPC/private/public endpoint settings; those are managed control-plane and VPC settings configured via the AWS console, CLI, or IaC. Refer to the Manual Steps section for how to review and change the cluster’s networking configuration.
      </Accordion>

      <Accordion title="Automation">
        ```bash theme={null}
        #!/usr/bin/env bash
        #
        # Report EKS cluster endpoint and node IP exposure
        # Requirements:
        #   - aws CLI configured with permissions to describe EKS clusters
        #   - kubectl configured for each target cluster context
        #   - jq installed
        #
        # Usage:
        #   ./eks_private_nodes_report.sh               # use all kubecontexts
        #   KUBECONFIG=/path/to/kubeconfig ./eks_private_nodes_report.sh

        set -euo pipefail

        if ! command -v aws >/dev/null 2>&1; then
          echo "ERROR: aws CLI not found in PATH" >&2
          exit 1
        fi

        if ! command -v kubectl >/dev/null 2>&1; then
          echo "ERROR: kubectl not found in PATH" >&2
          exit 1
        fi

        if ! command -v jq >/dev/null 2>&1; then
          echo "ERROR: jq not found in PATH" >&2
          exit 1
        fi

        contexts=$(kubectl config get-contexts -o name 2>/dev/null || true)
        if [ -z "${contexts}" ]; then
          echo "No kubectl contexts found."
          exit 0
        fi

        echo "Cluster,Context,AWS_Region,ControlPlaneEndpointPublicAccess,ControlPlaneEndpointPrivateAccess,PublicAccessCidrs,AnyNodeHasPublicIP,NodeDetails"

        for ctx in $contexts; do
          # Switch context
          kubectl config use-context "$ctx" >/dev/null

          # Try to infer EKS cluster name and region from aws-auth ConfigMap and/or ARN
          cluster_name=""
          region=""

          # First, try cluster-info (EKS ARNs usually contain cluster name)
          server_url=$(kubectl cluster-info | awk '/is running at/ {print $NF}' | head -n1 || true)
          # Example EKS endpoint: https://XXXXXXXX.gr7.us-west-2.eks.amazonaws.com
          if [[ "$server_url" =~ eks\.amazonaws\.com ]]; then
            host=$(printf "%s" "$server_url" | sed -E 's@https?://([^/]+)/?.*@\1@')
            # region is the 3rd label from the right for EKS endpoints: xxxxx.region.eks.amazonaws.com
            region=$(printf "%s" "$host" | awk -F. '{print $(NF-2)"-"$(NF-1)"-"$NF}' | sed -E 's/\.eks\.amazonaws\.com$//' || true)
          fi

          # If region parsing failed, fall back to an env var AWS_REGION/AWS_DEFAULT_REGION if set
          if [ -z "$region" ]; then
            region="${AWS_REGION:-${AWS_DEFAULT_REGION:-}}"
          fi

          # Try to get cluster name from aws-auth ConfigMap (not always present)
          aws_auth_ns="kube-system"
          if kubectl get cm aws-auth -n "$aws_auth_ns" >/dev/null 2>&1; then
            # This just confirms it's an EKS-like cluster but not the EKS cluster name
            :
          fi

          # As there's no reliable way via kubectl to infer EKS cluster name, require user mapping via context name
          # Assumption: kubectl context name == EKS cluster name (common practice with aws eks update-kubeconfig)
          cluster_name="$ctx"

          eks_desc=""
          if [ -n "$cluster_name" ] && [ -n "$region" ]; then
            eks_desc=$(aws eks describe-cluster \
              --name "$cluster_name" \
              --region "$region" \
              --output json 2>/dev/null || true)
          fi

          if [ -z "$eks_desc" ]; then
            # Not an EKS cluster (or cannot describe). We still check node external IPs via kubectl.
            endpoint_public="unknown-not-eks-or-no-perms"
            endpoint_private="unknown-not-eks-or-no-perms"
            public_cidrs="unknown-not-eks-or-no-perms"
          else
            endpoint_public=$(printf "%s" "$eks_desc" | jq -r '.cluster.resourcesVpcConfig.endpointPublicAccess')
            endpoint_private=$(printf "%s" "$eks_desc" | jq -r '.cluster.resourcesVpcConfig.endpointPrivateAccess')
            public_cidrs=$(printf "%s" "$eks_desc" | jq -r '.cluster.resourcesVpcConfig.publicAccessCidrs | join(";")')
          fi

          # Gather node IP information
          # Any node having an ExternalIP is a potential problem for this control
          # (You must verify in your cloud console if those are truly public IPs and routable.)
          nodes_json=$(kubectl get nodes -o json 2>/dev/null || true)
          if [ -z "$nodes_json" ]; then
            any_node_public="no-nodes"
            node_details="no-nodes"
          else
            any_ext=$(printf "%s" "$nodes_json" | jq '[.items[].status.addresses[] | select(.type=="ExternalIP")] | length > 0')
            any_node_public="$any_ext"
            # Collect concise node/IP summary
            node_details=$(printf "%s" "$nodes_json" |
              jq -r '.items[] |
                {
                  name: .metadata.name,
                  internalIPs: ([.status.addresses[] | select(.type=="InternalIP") | .address] | join("|")),
                  externalIPs: ([.status.addresses[] | select(.type=="ExternalIP") | .address] | join("|"))
                } |
                "\(.name):internal=\(.internalIPs),external=\(.externalIPs)"' |
                paste -sd ";" -)
            [ -z "$node_details" ] && node_details="none"
          fi

          echo "${cluster_name},${ctx},${region:-unknown},${endpoint_public},${endpoint_private},\"${public_cidrs}\",${any_node_public},\"${node_details}\""
        done
        ```

        Explanation of results (what indicates a problem):

        * For control-plane / cluster endpoint:
          * `ControlPlaneEndpointPublicAccess == true` without a justified restriction in `PublicAccessCidrs` (e.g., it is `0.0.0.0/0` or very broad) requires review.
          * `ControlPlaneEndpointPrivateAccess == false` means the API server is not reachable purely over private networking and should be reviewed.

        * For worker nodes:
          * `AnyNodeHasPublicIP == true` means at least one node reports an `ExternalIP`.
            * This indicates a potential violation of “Private Nodes only” and those nodes must be checked in the AWS console/VPC to confirm whether the ExternalIP is a real, routable public IP.
          * `NodeDetails` shows, per node, the internal and external addresses. Any non-empty `external=` field marks that node for manual review.

        This script is read-only: it does not change any configuration. Use it from any machine with kubectl and aws CLI access.
      </Accordion>
    </AccordionGroup>
  </Tab>
</Tabs>
