Skip to main content

Clusters Are Created With Private Endpoint Enabled And

More Info:

Create clusters with a private control plane endpoint and disable public access, so the Kubernetes API is not reachable from outside the node network when not required.

Risk Level

Critical

Address

Security

Compliance Standards

  • CIS OKE

Triage and Remediation

Remediation

Manual Steps
  1. Identify the cluster and current endpoint configuration

    • On any machine with access to your Oracle tenancy and CLI configured, run:
      oci ce cluster list \
      --compartment-id <OCID_OF_COMPARTMENT> \
      --all \
      --output table \
      --query "data[].{Name:name, OCID:id, EndpointConfig:endpoint-config}"
    • Note for each cluster:
      • is-public-ip-enabled (public endpoint on/off)
      • is-private-endpoint-enabled (private endpoint on/off)
      • Any nsg-ids or subnet-id associated with the endpoint.
  2. Review access requirements and network design

    • With your platform/network/application owners, decide for each cluster:
      • Do administrators or CI/CD systems outside the VCN (or connected networks like VPN/DRG/peering) need direct API access?
      • Can all required API consumers run from within the VCN (bastion host, jumpbox, VPN, private peering, etc.)?
    • If all legitimate access can be provided from within private networks, plan to:
      • Enable private endpoint.
      • Disable public endpoint.
  3. Check for dependencies on the public API endpoint

    • On any machine with kubectl access to the cluster, inspect the current API server URL used by admins/automation:
      kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}'; echo
    • Inventory all systems that use this endpoint (IDPs, CI/CD, monitoring, external operators, developer tools).
    • For each, decide if:
      • It can be moved inside the VCN / over VPN / over private peering, or
      • It strictly requires public access (in which case document and formally accept the risk as an exception).
  4. Plan and apply configuration changes in OCI

    • If you determine public access is not required for a cluster:
      • In the OCI Console:
        1. Go to “Developer Services” → “Kubernetes Clusters (OKE)” → select the cluster.
        2. Locate the “Endpoint” or “Cluster Access” section.
        3. Ensure a private endpoint is configured in an appropriate private subnet and NSGs.
        4. Disable the public endpoint / public access setting.
    • If using IaC (Terraform), review your code for the cluster resource, for example:
      • Ensure:
        endpoint_config {
        is_private_endpoint_enabled = true
        is_public_endpoint_enabled = false
        }
      • Apply the change with your standard pipeline (e.g., terraform plan / terraform apply) following your change-management process.
  5. Verify endpoint exposure after the change

    • On any machine with OCI CLI:
      oci ce cluster get --cluster-id <CLUSTER_OCID> --query "data.endpoint-config" --output json
    • Confirm:
      • "isPrivateEndpointEnabled": true
      • "isPublicEndpointEnabled": false (or equivalent field for public IP).
    • From an external network not connected to the VCN, confirm that the previous public API endpoint is no longer reachable (e.g., TLS handshake or curl now fails / times out).
  6. Update access patterns and documentation

    • Ensure all administrators and automation now use the private API endpoint (update kubeconfig distributions, bastion/jumpbox access, VPN instructions).
    • Document:
      • The decision to disable public access (or the justified exception if you kept it).
      • The VCN/subnet/NSG design that provides required private access to the Kubernetes API.
Using kubectl

kubectl cannot be used to enable a private control‑plane endpoint or disable public API access, because these settings are only configurable on the managed control plane via your cloud provider’s console, CLI, or IaC definitions. Please refer to the Manual Steps section for guidance on reviewing and updating the cluster’s control‑plane networking configuration.

Automation
#!/usr/bin/env bash
#
# Report OKE cluster endpoint exposure across compartments and regions.
# Requirements:
# - OCI CLI installed and configured (https://docs.oracle.com/en-us/iaas/Content/API/SDKDocs/cliinstall.htm)
# - jq installed
#
# Run location:
# - Any machine with OCI CLI access and permissions to list compartments and clusters.

set -euo pipefail

# ----- CONFIGURATION -----
# Optional: limit to specific regions or compartments by editing these arrays.
REGIONS=() # e.g. REGIONS=("eu-frankfurt-1" "us-ashburn-1")
COMPARTMENT_OCIDS=() # e.g. COMPARTMENT_OCIDS=("ocid1.compartment.oc1..xxxx")

# ----- HELPER FUNCTIONS -----
oci_json() {
oci "$@" --output json
}

get_all_regions() {
if ((${#REGIONS[@]} > 0)); then
printf '%s\n' "${REGIONS[@]}"
else
oci_json iam region list | jq -r '.data[].name'
fi
}

get_all_compartments() {
# Returns all active compartments in the tenancy (excluding root),
# unless COMPARTMENT_OCIDS is explicitly set.
if ((${#COMPARTMENT_OCIDS[@]} > 0)); then
printf '%s\n' "${COMPARTMENT_OCIDS[@]}"
else
local tenancy_ocid
tenancy_ocid="$(oci_json iam compartment list --compartment-id-in-subtree false --all | jq -r '.data[] | select(.["compartment-id"] == null) | .id')"

oci_json iam compartment list \
--compartment-id "${tenancy_ocid}" \
--compartment-id-in-subtree true \
--all \
| jq -r '.data[] | select(.lifecycle-state=="ACTIVE") | .id'
fi
}

echo "Scanning OKE clusters for API endpoint exposure..."
echo

header="region,compartment_name,cluster_name,cluster_ocid,endpoint_public_hostname,endpoint_private_endpoint_ip,endpoint_is_public_ip_enabled,endpoint_is_private_endpoint,has_public_access_issue"
echo "${header}"

for region in $(get_all_regions); do
export OCI_CLI_REGION="${region}"

# Map compartment OCID -> name (for readability in output)
declare -A COMP_NAME
while read -r cid cname; do
COMP_NAME["$cid"]="$cname"
done < <(
oci_json iam compartment list --compartment-id-in-subtree true --all \
| jq -r '.data[] | select(.lifecycle-state=="ACTIVE") | "\(.id) \(.name)"'
)

for compartment in $(get_all_compartments); do
clusters_json="$(oci_json ce cluster list --compartment-id "${compartment}" --all 2>/dev/null || echo '{}')"
cluster_count="$(jq '.data | length' <<<"${clusters_json}")"
if [[ "${cluster_count}" -eq 0 ]]; then
continue
fi

echo "${clusters_json}" | jq -c '.data[]' | while read -r c; do
cluster_id="$(jq -r '.id' <<<"${c}")"
cluster_name="$(jq -r '.name' <<<"${c}")"

# Get full cluster details (includes endpoint config)
detail="$(oci_json ce cluster get --cluster-id "${cluster_id}" 2>/dev/null | jq -c '.data')"

ep_host="$(jq -r '.endpoints.kubernetes' <<<"${detail}" 2>/dev/null || echo "null")"
ep_public_ip_enabled="$(jq -r '.endpoint-config.is-public-ip-enabled // "unknown"' <<<"${detail}" 2>/dev/null)"
ep_is_private_endpoint="$(jq -r '.endpoint-config.is-private-endpoint // "unknown"' <<<"${detail}" 2>/dev/null)"
ep_private_ip="$(jq -r '.endpoint-config.private-endpoint-ip // "null"' <<<"${detail}" 2>/dev/null)"

# Define "problem" per CIS: public endpoint enabled (is-public-ip-enabled == true)
# or not using private endpoint (is-private-endpoint != true) when private connectivity is possible.
issue="false"
if [[ "${ep_public_ip_enabled}" == "true" ]]; then
issue="true"
fi

comp_name="${COMP_NAME[${compartment}]:-unknown}"

printf '%s,%s,%s,%s,%s,%s,%s,%s,%s\n' \
"${region}" \
"${comp_name}" \
"${cluster_name}" \
"${cluster_id}" \
"${ep_host}" \
"${ep_private_ip}" \
"${ep_public_ip_enabled}" \
"${ep_is_private_endpoint}" \
"${issue}"
done
done
done

cat <<'EOF'

INTERPRETING RESULTS
--------------------
Each line describes an OKE cluster:

- endpoint_is_public_ip_enabled:
- "true" -> The Kubernetes API is reachable via a public IP (POTENTIAL PROBLEM)
- "false" -> Public IP disabled
- "unknown" -> Could not determine (review in console/CLI manually)

- endpoint_is_private_endpoint:
- "true" -> Cluster uses a private endpoint in your VCN
- "false" -> Private endpoint not used
- "unknown" -> Could not determine (review manually)

- has_public_access_issue:
- "true" -> Fails the intent of CIS OKE 5.4.2: public API access is enabled
- "false" -> Does not obviously violate the control, but still confirm design intent

Flagged rows ("has_public_access_issue=true") should be reviewed to decide if:
- Public API access is strictly required for operations, AND
- Compensating controls (IP allowlisting, VPN, bastion, etc.) are in place.

This script only reports configuration; disabling public access or switching to a private
endpoint must be done manually via OCI Console/CLI/Terraform following your change process.
EOF