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
- Remediation
Remediation
Manual Steps
-
Identify whether node pools use public IPs (OCI CLI – any machine with OCI CLI access)
# List all node pools in the compartmentoci 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 tableIf
isPublicIpistruefor any node pool, those nodes are not private. -
Confirm at the instance level that worker nodes do not have public IPs (OCI CLI – any machine with OCI CLI access)
# 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 tableAny non-empty
publicIpindicates non‑private nodes. -
Decide on remediation strategy (design decision – no command)
- If any node pool has
is-public-ip-enabled = trueor nodes showpublicIpvalues, 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.
- If any node pool has
-
Create a new private-only node pool (remediation – any machine with OCI CLI access)
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).
-
Migrate workloads and remove old public node pools (any machine with kubectl and OCI CLI access)
- Cordon and drain old public nodes:
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):
oci ce node-pool delete \--node-pool-id <OCID_OF_OLD_PUBLIC_NODE_POOL> \--force
- Cordon and drain old public nodes:
-
Verify remediation (OCI CLI – any machine with OCI CLI access)
# Re-check node pool configoci 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 instancesoci 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 tableConfirm all node pools show
isPublicIp = falseand all worker node instances have emptypublicIpvalues.
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.
Automation
#!/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.