Skip to main content

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)
  • BSI C5 (Germany)
  • Brazil LGPD
  • CCPA / CPRA (California)
  • CIS Critical Security Controls v8
  • CIS OKE
  • 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

Remediation

Manual Steps
  1. Identify node IP exposure for the cluster

    • Run on: any machine with OCI CLI access
    • Command (replace with your cluster OCID):
      oci ce cluster get \
      --cluster-id ocid1.cluster.oc1..xxxxxxxxxxxxxxxxxxxxxxxxxxxx \
      --query 'data.{name:name,endpoint:{"public":endpoints.publicEndpoint,"private":endpoints.privateEndpoint}}' \
      --output table
    • Review: Note whether the cluster is using a public endpoint and which networks it is attached to; this informs whether worker nodes are expected to be private.
  2. List and review node pools for public vs private networking

    • Run on: any machine with OCI CLI access
    • Command (replace with your cluster OCID):
      oci ce node-pool list \
      --compartment-id ocid1.compartment.oc1..xxxxxxxxxxxxxxxxxxxxxxxxxxxx \
      --cluster-id ocid1.cluster.oc1..xxxxxxxxxxxxxxxxxxxxxxxxxxxx \
      --all \
      --output table
    • Then inspect each node pool in detail:
      oci ce node-pool get \
      --node-pool-id ocid1.nodepool.oc1..xxxxxxxxxxxxxxxxxxxxxxxxxxxx \
      --query 'data.{name:name,placementConfigs:nodeConfigDetails.placementConfigs,subnetIds:nodeConfigDetails.subnetIds,nodeSource:nodeSource}' \
      --output json
    • Review: Identify which subnet(s) each node pool uses; record the subnet OCIDs.
  3. Determine whether those subnets allocate public IP addresses

    • Run on: any machine with OCI CLI access
    • For each subnet OCID found in step 2:
      oci network subnet get \
      --subnet-id ocid1.subnet.oc1..xxxxxxxxxxxxxxxxxxxxxxxxxxxx \
      --query 'data.{displayName:displayName,availDomain:availabilityDomain,publicSubnet:prohibitPublicIpOnVnic,routeTableId:routeTableId}' \
      --output json
    • Interpretation:
      • "publicSubnet": false (or prohibitPublicIpOnVnic: false) → subnet allows public IPs on VNICs (nodes can be public).
      • "publicSubnet": true (or prohibitPublicIpOnVnic: true) → subnet prohibits public IPs on VNICs (nodes are private).
  4. Verify if any existing worker nodes have public IPs assigned

    • Run on: any machine with OCI CLI access
    • List instances for each node pool’s subnet/VCN (adjust filters as needed):
      oci compute instance list \
      --compartment-id ocid1.compartment.oc1..xxxxxxxxxxxxxxxxxxxxxxxxxxxx \
      --lifecycle-state RUNNING \
      --output json > /tmp/oke_instances.json
      jq -r '.data[] | {id,displayName,"subnetId":\"\"} | @json' /tmp/oke_instances.json
    • For a specific instance OCID, inspect its VNICs:
      oci compute instance list-vnics \
      --instance-id ocid1.instance.oc1..xxxxxxxxxxxxxxxxxxxxxxxxxxxx \
      --query 'data[].{displayName:displayName,privateIp:privateIp,publicIp:publicIp}' \
      --output table
    • Review: Any non-empty publicIp values indicate nodes not conforming to the private-node requirement.
  5. Decision and corrective action planning

    • If any node pools use subnets that allow public IPs or any node VNICs have public IPs, decide whether:
      • To migrate to private node pools:
        • Create new node pools using subnets where prohibitPublicIpOnVnic: true.
        • Cordon/Drain workloads from old node pools and then delete those node pools.
      • Or to recreate the entire cluster in a VCN/Subnet design that uses only private subnets for worker nodes and (if desired) a private control-plane endpoint.
    • These changes are performed via OCI Console, Terraform, or OCI CLI; there is no in-place toggle to “make existing nodes private” without node pool or cluster recreation.
  6. Post-change verification

    • After creating new private node pools or a new private cluster and migrating workloads, re-run:
      # Verify node pool subnets
      oci ce node-pool list \
      --compartment-id ocid1.compartment.oc1..xxxxxxxxxxxxxxxxxxxxxxxxxxxx \
      --cluster-id ocid1.cluster.oc1..xxxxxxxxxxxxxxxxxxxxxxxxxxxx \
      --all --output table

      # Verify subnets disallow public IPs
      oci network subnet get \
      --subnet-id ocid1.subnet.oc1..yyyyyyyyyyyyyyyyyyyyyyyyyyyy \
      --query 'data.prohibitPublicIpOnVnic' \
      --output table

      # Spot-check nodes for public IPs
      oci compute instance list-vnics \
      --instance-id ocid1.instance.oc1..zzzzzzzzzzzzzzzzzzzzzzzzzz \
      --query 'data[].publicIp' \
      --output table
    • Confirm all worker-node VNICs report no publicIp and all node subnets have prohibitPublicIpOnVnic: true; the cluster then meets the private-nodes requirement.
Using kubectl

kubectl cannot change whether cluster nodes have public or private IPs; this is controlled in the managed control-plane / cloud provider configuration (for example, in the cluster’s networking or node pool settings). Use the cloud provider console, CLI, or IaC tooling instead and follow the guidance in the Manual Steps section.

Automation
#!/usr/bin/env bash
#
# Check whether Kubernetes nodes have public IP addresses.
# Run from any machine with kubectl access and kubeconfig for the target cluster.
#
# Requirements:
# - kubectl
# - jq

set -euo pipefail

echo "Checking for public IP addresses on Kubernetes nodes..."
echo

kubectl get nodes -o json | jq -r '
.items[]
| {
name: .metadata.name,
internalIPs: (
.status.addresses[]
| select(.type=="InternalIP")
| .address
) // [],
externalIPs: (
.status.addresses[]
| select(.type=="ExternalIP")
| .address
) // []
}
| @json
' | while read -r line; do
name=$(echo "$line" | jq -r '.name')
internal_ips=$(echo "$line" | jq -r '.internalIPs | join(",")')
external_ips=$(echo "$line" | jq -r '.externalIPs | join(",")')

if [[ "$external_ips" != "" ]]; then
status="NON-COMPLIANT"
else
status="OK"
fi

printf "%-40s | internalIPs=%-20s | externalIPs=%-20s | %s\n" \
"$name" "$internal_ips" "${external_ips:-none}" "$status"
done

echo
echo "Interpretation:"
echo " - Nodes with status 'OK' have no ExternalIP reported by the API."
echo " - Nodes with status 'NON-COMPLIANT' expose an ExternalIP and must be reviewed."
echo
echo "Note:"
echo " - This script surfaces nodes with ExternalIP as seen by the Kubernetes API."
echo " - For managed offerings (OKE/EKS/AKS/GKE), you must verify cluster/node-pool"
echo " configuration in the cloud console/CLI/IaC to confirm whether nodes are"
echo " created as private-only and to remediate any NON-COMPLIANT nodes."

Output indicating a problem

Any line where the final column is NON-COMPLIANT (i.e., the externalIPs column is non-empty) indicates a node that has an ExternalIP and should be reviewed against the requirement that cluster nodes be private-only.