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
Remediation
Manual Steps
-
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.
-
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.
-
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(orprohibitPublicIpOnVnic: false) → subnet allows public IPs on VNICs (nodes can be public)."publicSubnet": true(orprohibitPublicIpOnVnic: true) → subnet prohibits public IPs on VNICs (nodes are private).
-
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.jsonjq -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
publicIpvalues indicate nodes not conforming to the private-node requirement.
-
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.
- Create new node pools using subnets where
- 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.
- To migrate to private node pools:
- 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.
- If any node pools use subnets that allow public IPs or any node VNICs have public IPs, decide whether:
-
Post-change verification
- After creating new private node pools or a new private cluster and migrating workloads, re-run:
# Verify node pool subnetsoci ce node-pool list \--compartment-id ocid1.compartment.oc1..xxxxxxxxxxxxxxxxxxxxxxxxxxxx \--cluster-id ocid1.cluster.oc1..xxxxxxxxxxxxxxxxxxxxxxxxxxxx \--all --output table# Verify subnets disallow public IPsoci network subnet get \--subnet-id ocid1.subnet.oc1..yyyyyyyyyyyyyyyyyyyyyyyyyyyy \--query 'data.prohibitPublicIpOnVnic' \--output table# Spot-check nodes for public IPsoci compute instance list-vnics \--instance-id ocid1.instance.oc1..zzzzzzzzzzzzzzzzzzzzzzzzzz \--query 'data[].publicIp' \--output table
- Confirm all worker-node VNICs report no
publicIpand all node subnets haveprohibitPublicIpOnVnic: true; the cluster then meets the private-nodes requirement.
- After creating new private node pools or a new private cluster and migrating workloads, re-run:
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.