> ## 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:

Create clusters with private nodes that have only private IP addresses, preventing direct public network exposure of worker nodes.

### Risk Level

High

### Address

Security

### Compliance Standards

* CIS OKE

### Triage and Remediation

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

    <AccordionGroup>
      <Accordion title="Manual Steps" defaultOpen="true">
        1. **Identify whether node pools use public IPs (OCI CLI – any machine with OCI CLI access)**
           ```bash theme={null}
           # List all node pools in the compartment
           oci ce node-pool list \
             --compartment-id <OCID_OF_COMPARTMENT> \
             --cluster-id <OCID_OF_CLUSTER> \
             --all

           # For each node pool OCID found, describe it and check `node-config-details.is-public-ip-enabled`
           oci ce node-pool get \
             --node-pool-id <OCID_OF_NODE_POOL> \
             --query "data.{name:name, isPublicIp:node-config-details.is-public-ip-enabled}" \
             --output table
           ```
           If `isPublicIp` is `true` for any node pool, those nodes are not private.

        2. **Confirm at the instance level that worker nodes do not have public IPs (OCI CLI – any machine with OCI CLI access)**
           ```bash theme={null}
           # List instances created for a node pool (by display-name pattern or freeform tags)
           oci compute instance list \
             --compartment-id <OCID_OF_COMPARTMENT> \
             --display-name <NODE_POOL_NAME_PREFIX> \
             --all \
             --query "data[].{name:\"display-name\", publicIp:\"public-ip\"}" \
             --output table
           ```
           Any non-empty `publicIp` indicates non‑private nodes.

        3. **Decide on remediation strategy (design decision – no command)**
           * If any node pool has `is-public-ip-enabled = true` or nodes show `publicIp` values, decide whether to:
             * Recreate the node pool(s) with private nodes only, and cordon/drain/migrate workloads, or
             * Create new private-only node pools and then delete the public node pools after workload migration.
           * Ensure you have private connectivity (VCN, subnets, NAT/egress, bastion, etc.) to manage private nodes.

        4. **Create a new private-only node pool (remediation – any machine with OCI CLI access)**
           ```bash theme={null}
           oci ce node-pool create \
             --compartment-id <OCID_OF_COMPARTMENT> \
             --cluster-id <OCID_OF_CLUSTER> \
             --name <NEW_PRIVATE_NODEPOOL_NAME> \
             --kubernetes-version <K8S_VERSION> \
             --node-shape <SHAPE> \
             --node-metadata '{}' \
             --node-config-details '{
               "placementConfigs": [{
                 "availabilityDomain": "<AD_NAME>",
                 "subnetId": "<OCID_OF_PRIVATE_SUBNET>"
               }],
               "size": <DESIRED_NODE_COUNT>,
               "isPublicIpEnabled": false
             }'
           ```
           Ensure the subnet is private (no public IP assignment, appropriate route tables/NSGs).

        5. **Migrate workloads and remove old public node pools (any machine with kubectl and OCI CLI access)**
           * Cordon and drain old public nodes:
             ```bash theme={null}
             kubectl get nodes -o wide
             # For each node with a public IP:
             kubectl cordon <NODE_NAME>
             kubectl drain <NODE_NAME> --ignore-daemonsets --delete-emptydir-data
             ```
           * After workloads are stable on the new private node pool, delete the old node pool(s):
             ```bash theme={null}
             oci ce node-pool delete \
               --node-pool-id <OCID_OF_OLD_PUBLIC_NODE_POOL> \
               --force
             ```

        6. **Verify remediation (OCI CLI – any machine with OCI CLI access)**
           ```bash theme={null}
           # Re-check node pool config
           oci ce node-pool list \
             --compartment-id <OCID_OF_COMPARTMENT> \
             --cluster-id <OCID_OF_CLUSTER> \
             --all \
             --query "data[].{name:name, isPublicIp:node-config-details.is-public-ip-enabled}" \
             --output table

           # Re-check instances
           oci compute instance list \
             --compartment-id <OCID_OF_COMPARTMENT> \
             --all \
             --query "data[?contains(\"display-name\", '<CLUSTER_OR_NODEPOOL_NAME_PREFIX>')].{name:\"display-name\", publicIp:\"public-ip\"}" \
             --output table
           ```
           Confirm all node pools show `isPublicIp = false` and all worker node instances have empty `publicIp` values.
      </Accordion>

      <Accordion title="Using kubectl">
        kubectl cannot change whether cluster nodes use private or public IP addresses; that setting is managed in the cloud provider’s control‑plane / cluster configuration (console, CLI, or IaC) where the cluster is created or updated. Refer to the Manual Steps section for how to review and remediate this setting in your environment.
      </Accordion>

      <Accordion title="Automation">
        ```bash theme={null}
        #!/usr/bin/env bash
        #
        # Report whether Kubernetes nodes appear to be privately or publicly addressed.
        # Run on any machine with kubectl access and KUBECONFIG set for the target cluster.

        set -euo pipefail

        echo "Checking node IP exposure for cluster: $(kubectl config current-context)"
        echo "Timestamp: $(date -Iseconds)"
        echo

        # 1) List all nodes and their Internal/External IPs from the Kubernetes API
        echo "=== Node IPs from Kubernetes Node objects ==="
        kubectl get nodes -o json \
          | jq -r '
            .items[]
            | {
                name: .metadata.name,
                addresses: (
                  .status.addresses
                  | map({(.type): .address})
                  | add
                )
              }
            | [
                .name,
                ( .addresses.InternalIP // "NONE" ),
                ( .addresses.ExternalIP // "NONE" )
              ]
            | @tsv
          ' \
          | awk 'BEGIN { printf "%-40s %-18s %-18s\n", "NODE", "INTERNAL_IP", "EXTERNAL_IP";
                          print "--------------------------------------------------------------------------------" }
                 { printf "%-40s %-18s %-18s\n", $1, $2, $3 }'
        echo

        # 2) Highlight nodes that have an ExternalIP in the Kubernetes API
        echo "=== Nodes with ExternalIP set (potentially PUBLICLY EXPOSED) ==="
        HAS_EXTERNAL=$(kubectl get nodes -o json \
          | jq -r '
              .items[]
              | {
                  name: .metadata.name,
                  external: (
                    .status.addresses[]
                    | select(.type=="ExternalIP")
                    | .address
                  )
                }
              | select(.external != null)
              | [.name, .external]
              | @tsv
            ' || true)

        if [ -z "$HAS_EXTERNAL" ]; then
          echo "No nodes report an ExternalIP in the Kubernetes API."
        else
          printf "%-40s %-18s\n" "NODE" "EXTERNAL_IP"
          echo "--------------------------------------------------------------"
          printf '%s\n' "$HAS_EXTERNAL" \
            | awk '{ printf "%-40s %-18s\n", $1, $2 }'
        fi
        echo

        # 3) Optional: try to infer public vs private via RFC1918 ranges
        #    (This is heuristic; final determination must be done in the cloud console/IaC.)
        echo "=== Heuristic: Nodes with non-RFC1918 InternalIP (suspicious) ==="
        kubectl get nodes -o json \
          | jq -r '
            .items[]
            | {
                name: .metadata.name,
                addresses: (
                  .status.addresses
                  | map({(.type): .address})
                  | add
                )
              }
            | select(.addresses.InternalIP != null)
            | select(
                (.addresses.InternalIP
                  | test("^10\\.")       == false
                  and test("^192\\.168\\.") == false
                  and test("^172\\.(1[6-9]|2[0-9]|3[0-1])\\.") == false
                )
              )
            | [.name, .addresses.InternalIP]
            | @tsv
          ' | awk 'BEGIN {
                     printf "%-40s %-18s\n", "NODE", "INTERNAL_IP (NON-RFC1918)"
                     print "--------------------------------------------------------------"
                   }
                   { printf "%-40s %-18s\n", $1, $2 }' || true

        echo
        echo "INTERPRETATION:"
        echo "- Any node listed in 'Nodes with ExternalIP set' must be reviewed: CIS requires nodes to"
        echo "  NOT have public IPs (private nodes only). If any appear there, this is a problem."
        echo "- Nodes shown under the heuristic section use non-RFC1918 InternalIP ranges and should be"
        echo "  reviewed in the cloud provider console/IaC to confirm whether they are actually public."
        echo
        echo "Next steps (manual review required):"
        echo "- In your cloud provider console or IaC, confirm that worker node groups / node pools"
        echo "  are configured as private (no public IPs) and attached to private subnets only."
        ```

        **What output indicates a problem**

        * If the “Nodes with ExternalIP set” section lists any node and IP, that node appears to be publicly reachable and violates the requirement for private nodes.
        * If the heuristic section lists nodes with non‑RFC1918 `InternalIP`, those nodes might be using publicly routable addresses and should be manually verified in the cloud provider configuration.
      </Accordion>
    </AccordionGroup>
  </Tab>
</Tabs>
