Private AKS Clusters in Azure

By default, the AKS API server is accessible from the public internet. Anyone can attempt to authenticate against it. In security-conscious environments — regulated industries, enterprise production environments, or any cluster running sensitive workloads — that is not acceptable. A private AKS cluster places the API server entirely inside your Azure Virtual Network, invisible to the public internet.

What makes a cluster “private”

In a standard AKS cluster, the Kubernetes API server has a public FQDN like my-cluster-dns-12345.hcp.eastus.azmk8s.io that resolves to a public IP. Anyone on the internet can send requests to it (though they still need valid credentials to authenticate).

A private AKS cluster creates the API server behind a private endpoint inside your VNet. The API server’s FQDN resolves to a private IP address (from your VNet subnet). No public IP exists. The API server is completely unreachable from outside your VNet.

The worker nodes communicate with the API server via private IP as well, since they are in the same VNet. Nothing changes for pod-to-pod or pod-to-service communication — only the management plane (kubectl access) changes.

Creating a private cluster

# Create the VNet and subnet first (best practice for production)
az network vnet create \
  --resource-group my-aks-rg \
  --name my-vnet \
  --address-prefix 10.0.0.0/8 \
  --subnet-name aks-subnet \
  --subnet-prefix 10.240.0.0/16

SUBNET_ID=$(az network vnet subnet show \
  --resource-group my-aks-rg \
  --vnet-name my-vnet \
  --name aks-subnet \
  --query id -o tsv)

# Create the private AKS cluster
az aks create \
  --resource-group my-aks-rg \
  --name private-cluster \
  --enable-private-cluster \
  --vnet-subnet-id $SUBNET_ID \
  --network-plugin azure \
  --enable-managed-identity \
  --generate-ssh-keys

After creation, note that the cluster now has two FQDNs:

  • A private FQDN that resolves to a private IP within the VNet
  • No public FQDN at all (unlike the hybrid mode described below)
# Verify the cluster has no public FQDN
az aks show \
  --resource-group my-aks-rg \
  --name private-cluster \
  --query "privateFqdn" -o tsv

# This returns the private API server address
# e.g., private-cluster-abcd1234.privatelink.eastus.azmk8s.io

Connecting kubectl to a private cluster

Because the API server is private, you cannot run az aks get-credentials and connect directly unless your machine is in the same VNet (or connected via VPN/ExpressRoute).

The fastest option for getting started is the Azure CLI command tunnel:

# Use the Azure CLI to establish a tunnel to the private API server
az aks command invoke \
  --resource-group my-aks-rg \
  --name private-cluster \
  --command "kubectl get nodes"

# The command-invoke approach runs kubectl on Azure's infrastructure
# It does not require VPN or VNet connectivity from your local machine

For regular day-to-day use, set up proper network connectivity. Options:

  • Azure VPN Gateway — site-to-site or point-to-site VPN from your office or developer machines
  • Azure Bastion + Jump VM — a VM inside the VNet with a Bastion connection. Developers SSH to the jump VM and run kubectl there.
  • Azure ExpressRoute — private circuit from on-premises data center to Azure VNet
  • GitHub Codespaces or Azure Container Apps in VNet — for CI/CD pipelines and developer environments

Private DNS configuration

Private clusters use Azure Private DNS zones to resolve the API server FQDN to a private IP. AKS creates and manages this DNS zone automatically by default.

For environments where you manage DNS centrally:

# Create the cluster with custom private DNS zone
az aks create \
  --resource-group my-aks-rg \
  --name private-cluster \
  --enable-private-cluster \
  --private-dns-zone /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/privateDnsZones/privatelink.eastus.azmk8s.io \
  --generate-ssh-keys

# Or use the system-managed zone (simplest)
az aks create \
  --resource-group my-aks-rg \
  --name private-cluster \
  --enable-private-cluster \
  --private-dns-zone system \
  --generate-ssh-keys

If you have VNet peering, the private DNS zone needs to be linked to the peered VNets for the API server to be resolvable from those networks:

# Link a private DNS zone to a peered VNet
az network private-dns link vnet create \
  --resource-group MC_my-aks-rg_private-cluster_eastus \
  --zone-name "privatelink.eastus.azmk8s.io" \
  --name my-vnet-link \
  --virtual-network /subscriptions/{sub}/resourceGroups/other-rg/providers/Microsoft.Network/virtualNetworks/other-vnet \
  --registration-enabled false

Hybrid: private cluster with public FQDN option

For some scenarios — like CI/CD pipelines that run outside your VNet but need to deploy to the cluster — you can enable a public FQDN alongside the private endpoint. This does not re-expose the API server to the internet in the traditional sense; it still requires Azure AD authentication, but it resolves over the public internet:

# Enable public FQDN alongside private endpoint
az aks update \
  --resource-group my-aks-rg \
  --name private-cluster \
  --enable-public-fqdn

# Or start with public FQDN disabled and enable it later if needed
az aks update \
  --resource-group my-aks-rg \
  --name private-cluster \
  --disable-public-fqdn
Note

Re-enabling the public FQDN is a practical concession for teams using external CI/CD systems like GitHub Actions. For maximum security, keep the public FQDN disabled and use self-hosted GitHub Actions runners deployed inside the VNet instead.

Alternative: authorized IP ranges on public clusters

If a fully private cluster is too complex for your environment, you can restrict API server access to specific IP addresses on a public cluster:

# Create cluster with IP restrictions on API server
az aks create \
  --resource-group my-aks-rg \
  --name restricted-cluster \
  --api-server-authorized-ip-ranges "203.0.113.0/24,198.51.100.10/32" \
  --generate-ssh-keys

# Update IP ranges on existing cluster
az aks update \
  --resource-group my-aks-rg \
  --name restricted-cluster \
  --api-server-authorized-ip-ranges "203.0.113.0/24"

# Remove restrictions (re-open to all IPs - use with caution)
az aks update \
  --resource-group my-aks-rg \
  --name restricted-cluster \
  --api-server-authorized-ip-ranges ""

Authorized IP ranges are a good middle ground for teams that need public API access but want to limit it to known networks like office IPs or CI/CD runner IPs. The API server still has a public IP but only accepts connections from the allowlisted ranges.

CI/CD with private clusters

The most common challenge with private clusters is making CI/CD pipelines work. For Azure DevOps and GitHub Actions, the recommended pattern is a self-hosted agent/runner deployed inside the VNet:

# Deploy a self-hosted GitHub Actions runner in the VNet
# (Use the ARC - Actions Runner Controller)
helm repo add actions-runner-controller \
  https://actions-runner-controller.github.io/actions-runner-controller
helm repo update

helm install arc \
  --namespace arc-systems \
  --create-namespace \
  actions-runner-controller/actions-runner-controller \
  --set authSecret.create=true \
  --set authSecret.github_token="<your-github-pat>"

# Create a runner that has VNet access to the private cluster
cat <<EOF | kubectl apply -f -
apiVersion: actions.summerwind.dev/v1alpha1
kind: RunnerDeployment
metadata:
  name: github-runner
  namespace: arc-systems
spec:
  replicas: 2
  template:
    spec:
      repository: my-org/my-repo
      image: summerwind/actions-runner:latest
EOF

Common mistakes

  1. Not setting up VPN or Bastion before creating the private cluster. Once the cluster is private, you need network connectivity to access it. Plan your connectivity solution before creating the cluster, or you will be locked out immediately.
  2. Forgetting to link private DNS zones to peered VNets. If you have VNet peering (e.g., hub-and-spoke architecture), the private DNS zone for the cluster must be linked to all VNets that need to resolve the API server. Otherwise, connections time out even though the network path exists.
  3. Using az aks command invoke for all cluster operations. The command invoke feature is for occasional use. For regular operations, set up proper VPN connectivity and use kubectl normally. Command invoke adds latency and does not support all kubectl features well.
  4. Assuming private cluster means application traffic is also private. A private cluster with a LoadBalancer service still creates a public IP. Use internal load balancers and network policies to control application-level traffic separately from API server access.
  5. Not planning for node image updates and upgrades. Node VMs in a private cluster still need to pull updated images and OS patches. Ensure outbound internet access is available from nodes (via NAT Gateway or firewall egress rules), even though the API server is private.

Frequently asked questions

Can I convert an existing public AKS cluster to a private cluster?

Yes, with limitations. You can disable the public endpoint on an existing cluster with az aks update --disable-public-fqdn. However, switching from public to fully private (no public API server at all) requires recreating the cluster.

How do my developers access a private cluster without a public API server?

They need network connectivity to your VNet. Options include: Azure VPN Gateway, Azure ExpressRoute, Azure Bastion for jump hosts, or dev tools like Azure Container Apps or GitHub Codespaces that can be VNet-joined.

Does a private cluster prevent external traffic to my applications?

No. Private cluster refers only to the Kubernetes API server endpoint. Your applications deployed in the cluster can still have public LoadBalancer services with public IPs. Use network policies and internal load balancers to control application traffic separately.

Last verified: 19 March 2026 Cloud services change frequently. Verify details against official documentation before making infrastructure decisions.