> ## Documentation Index
> Fetch the complete documentation index at: https://cloudanix.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Ensure 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

<Tabs>
  <Tab title="Remediation">
    ### Remediation

    <AccordionGroup>
      <Accordion title="Manual Steps" defaultOpen="true">
        1. **Review current API server endpoints (Azure CLI – any machine with `az` and access):**
           ```bash theme={null}
           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:
             ```bash theme={null}
             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:
             ```bash theme={null}
             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):
             ```bash theme={null}
             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):
             ```bash theme={null}
             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):**
           ```bash theme={null}
           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.
      </Accordion>

      <Accordion title="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.
      </Accordion>

      <Accordion title="Automation">
        ```bash theme={null}
        #!/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.
      </Accordion>
    </AccordionGroup>
  </Tab>
</Tabs>
