Skip to main content

Clusters Use Private Endpoint With Public Access Disabled

More Info:

Create clusters with a private endpoint and disable the public API server endpoint so the control plane is only reachable from within your virtual network.

Risk Level

Critical

Address

Security

Compliance Standards

  • CIS AKS

Triage and Remediation

Remediation

Manual Steps
  1. Review current API server endpoints (Azure CLI – any machine with az and access):

    az aks show \
    --resource-group <RESOURCE_GROUP_NAME> \
    --name <CLUSTER_NAME> \
    --query "{privateCluster:apiServerAccessProfile.enablePrivateCluster, \
    privateFqdn:apiServerAccessProfile.privateFqdn, \
    authorizedIpRanges:apiServerAccessProfile.authorizedIpRanges, \
    enablePublicFqdn:apiServerAccessProfile.enablePublicFqdn}" \
    -o json
    • Confirm whether enablePrivateCluster/privateCluster is true (private endpoint enabled).
    • Confirm enablePublicFqdn is false and authorizedIpRanges is empty (no public access), or that public access is intentionally restricted to minimal CIDRs.
  2. Identify and review the AKS-managed private endpoint in the virtual network (Azure Portal or CLI – any machine):

    • In the Portal: go to the cluster → Networking → verify “Private cluster” is enabled and note the linked virtual network and subnet.
    • Or via CLI, list private endpoints and find the one targeting the AKS control plane:
      az network private-endpoint list \
      --resource-group <VNET_RESOURCE_GROUP> \
      -o table
    • Confirm the private endpoint is in the intended subnet and that network security groups/firewall rules allow required access from your admin/jump-host subnets.
  3. Check private DNS configuration for the cluster’s private endpoint (Azure CLI – any machine):

    • List private DNS zones and links:
      az network private-dns zone list -o table
      az network private-dns link vnet list \
      --resource-group <DNS_RESOURCE_GROUP> \
      --zone-name "privatelink.<region>.azmk8s.io" \
      -o table
    • Confirm there is a private DNS zone (for example, privatelink.<region>.azmk8s.io) and that it is linked to the same virtual network used by the cluster’s private endpoint with registrationEnabled as appropriate.
  4. Decide on desired exposure policy (governance decision – no command):

    • Decide if the cluster must be private-only (recommended) or if restricted public CIDRs are required for specific use cases.
    • Document approved admin networks (on-prem, VPN, peering, jump hosts) that should reach the private endpoint, and any exceptional public CIDRs if you will allow them.
  5. Apply configuration to enforce private-only or restricted public access (Azure CLI – any machine):

    • To disable the public API endpoint entirely (preferred):
      az aks update \
      --resource-group <RESOURCE_GROUP_NAME> \
      --name <CLUSTER_NAME> \
      --api-server-access-profile enablePublicFqdn=false
    • If you must keep a public endpoint but restrict it, supply specific CIDR ranges (and optionally hide the public FQDN):
      az aks update \
      --resource-group <RESOURCE_GROUP_NAME> \
      --name <CLUSTER_NAME> \
      --api-server-access-profile \
      authorizedIpRanges="<CIDR1>,<CIDR2>" \
      enablePublicFqdn=false
    • Ensure that private cluster mode is enabled at creation time for new clusters (cannot be toggled later), using --enable-private-cluster in your az aks create/IaC definitions.
  6. Verify final state matches the benchmark intent (Azure CLI – any machine):

    az aks show \
    --resource-group <RESOURCE_GROUP_NAME> \
    --name <CLUSTER_NAME> \
    --query "{privateCluster:apiServerAccessProfile.enablePrivateCluster, \
    privateFqdn:apiServerAccessProfile.privateFqdn, \
    authorizedIpRanges:apiServerAccessProfile.authorizedIpRanges, \
    enablePublicFqdn:apiServerAccessProfile.enablePublicFqdn}" \
    -o json
    • Confirm privateCluster is true, privateFqdn is set, enablePublicFqdn is false, and either authorizedIpRanges is [] (no public access) or restricted to the explicitly approved CIDRs.
Using kubectl

kubectl cannot change the API server’s public/private endpoint configuration because it is managed at the AKS control-plane / Azure resource level. Make the changes through the Azure portal, az CLI, or your IaC (ARM/Bicep/Terraform) instead, and follow the guidance in the Manual Steps section.

Automation
#!/usr/bin/env bash
#
# Report AKS API server endpoint exposure for all clusters in all subscriptions.
# Requires: az CLI logged in with sufficient RBAC to read AKS clusters.
# Runs on: any machine with az CLI access.

set -euo pipefail

echo "Enumerating all subscriptions..." >&2
subs_json=$(az account list --query '[].{id:id,name:name}' -o json)

if [ "$(echo "$subs_json" | jq 'length')" -eq 0 ]; then
echo "No subscriptions found. Are you logged in? (az login)" >&2
exit 1
fi

printf "subscriptionId,subscriptionName,resourceGroup,clusterName,privateClusterEnabled,enablePrivateEndpoint,fqdn,privateFqdn,apiServerAuthorizedIpRanges\n"

echo "$subs_json" | jq -r '.[] | @base64' | while read -r sub_b64; do
sub_id=$(echo "$sub_b64" | base64 -d | jq -r '.id')
sub_name=$(echo "$sub_b64" | base64 -d | jq -r '.name')

az account set --subscription "$sub_id" >/dev/null

clusters_json=$(az aks list -o json)
if [ "$(echo "$clusters_json" | jq 'length')" -eq 0 ]; then
continue
fi

echo "$clusters_json" | jq -r '.[] | @base64' | while read -r c_b64; do
rg=$(echo "$c_b64" | base64 -d | jq -r '.resourceGroup')
name=$(echo "$c_b64" | base64 -d | jq -r '.name')
privateClusterEnabled=$(echo "$c_b64" | base64 -d | jq -r '.apiServerAccessProfile.enablePrivateCluster // false')
enablePrivateEndpoint=$(echo "$c_b64" | base64 -d | jq -r '.apiServerAccessProfile.enablePrivateEndpoint // false')
fqdn=$(echo "$c_b64" | base64 -d | jq -r '.fqdn // ""')
privateFqdn=$(echo "$c_b64" | base64 -d | jq -r '.privateFqdn // ""')
apiServerAuthorizedIpRanges=$(echo "$c_b64" | base64 -d | jq -r '.apiServerAccessProfile.authorizedIpRanges // [] | join(" ")')

printf "%s,%s,%s,%s,%s,%s,%s,%s,%s\n" \
"$sub_id" \
"$sub_name" \
"$rg" \
"$name" \
"$privateClusterEnabled" \
"$enablePrivateEndpoint" \
"$fqdn" \
"$privateFqdn" \
"$apiServerAuthorizedIpRanges"
done
done

How to interpret the output (what indicates a problem):

  • Each line is one cluster.
  • Focus on these columns:
    • privateClusterEnabled and/or enablePrivateEndpoint should be true to indicate private endpoint usage.
    • fqdn represents the public API server endpoint (public FQDN).
    • privateFqdn represents the private endpoint DNS name.

Potential issues to flag manually:

  1. Public access still enabled:

    • fqdn is non-empty AND
    • Either:
      • privateClusterEnabled is false and enablePrivateEndpoint is false (no private endpoint at all), or
      • You have a policy that public endpoint must be fully disabled (even with IP restrictions).
  2. No private endpoint:

    • privateClusterEnabled=false AND enablePrivateEndpoint=false AND privateFqdn is empty.
  3. Weak restriction when public endpoint must be off:

    • If your requirement is “no public access at all”:
      • Any cluster with a non-empty fqdn should be reviewed, even if apiServerAuthorizedIpRanges is populated.

This script does not change configuration; it only surfaces clusters that need manual review and potential remediation via Azure CLI, portal, or IaC.