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
Remediation
Manual Steps
-
Review current API server endpoints (Azure CLI – any machine with
azand 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/privateClusteris true (private endpoint enabled). - Confirm
enablePublicFqdnis false andauthorizedIpRangesis empty (no public access), or that public access is intentionally restricted to minimal CIDRs.
- Confirm whether
-
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.
-
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 tableaz 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 withregistrationEnabledas appropriate.
- List private DNS zones and links:
-
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.
-
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-clusterin youraz aks create/IaC definitions.
- To disable the public API endpoint entirely (preferred):
-
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
privateClusteris true,privateFqdnis set,enablePublicFqdnis false, and eitherauthorizedIpRangesis[](no public access) or restricted to the explicitly approved CIDRs.
- Confirm
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:
privateClusterEnabledand/orenablePrivateEndpointshould betrueto indicate private endpoint usage.fqdnrepresents the public API server endpoint (public FQDN).privateFqdnrepresents the private endpoint DNS name.
Potential issues to flag manually:
-
Public access still enabled:
fqdnis non-empty AND- Either:
privateClusterEnabledisfalseandenablePrivateEndpointisfalse(no private endpoint at all), or- You have a policy that public endpoint must be fully disabled (even with IP restrictions).
-
No private endpoint:
privateClusterEnabled=falseANDenablePrivateEndpoint=falseANDprivateFqdnis empty.
-
Weak restriction when public endpoint must be off:
- If your requirement is “no public access at all”:
- Any cluster with a non-empty
fqdnshould be reviewed, even ifapiServerAuthorizedIpRangesis populated.
- Any cluster with a non-empty
- If your requirement is “no public access at all”:
This script does not change configuration; it only surfaces clusters that need manual review and potential remediation via Azure CLI, portal, or IaC.