Back to blog
AzureBest PracticesCloud SecurityKubernetesNetworking

AKS API Server Is Publicly Accessible: Lock Down Your Cluster Control Plane

Learn why a public AKS API server is a security risk and how to deploy private AKS clusters, restrict access with authorized IP ranges, and enforce it with policy.

TL;DR

This check flags AKS clusters whose Kubernetes API server is reachable over the public internet instead of being locked to a private endpoint. A public API server is a high-value target for credential stuffing and CVE exploitation. Fix it by deploying a private cluster, or at minimum lock down access with authorized IP ranges.

The Kubernetes API server is the front door to your entire cluster. Every kubectl command, every controller, every CI/CD deploy talks to it. When that front door faces the open internet, anyone who can resolve its FQDN can attempt to authenticate against it. The aks_noprivate check looks for AKS clusters that were created without a private cluster configuration, meaning the API server has a public endpoint.

This post walks through what the check catches, why an exposed API server is a real risk, and how to close it down without breaking your existing pipelines.


What this check detects

When you create an AKS cluster, Azure provisions a managed control plane that includes the Kubernetes API server. By default, that API server gets a public FQDN backed by a public IP. The check verifies whether the cluster was created as a private cluster, where the API server is instead exposed through a private endpoint inside your virtual network.

In practical terms, the check inspects the cluster's apiServerAccessProfile and fails when enablePrivateCluster is not set to true.

Note: A public API server is not the same as an unauthenticated one. AKS still requires valid credentials (Azure AD tokens or client certificates) to do anything. The risk is that the authentication and authorization surface is exposed to the entire internet rather than just your network.

You can confirm a cluster's current state with the Azure CLI:

az aks show \
  --resource-group my-rg \
  --name my-cluster \
  --query "apiServerAccessProfile" \
  -o json

If the output is null, or enablePrivateCluster is false, the API server is public.


Why it matters

A publicly reachable API server widens your attack surface in a few concrete ways.

It is a target for credential and token abuse

If an attacker obtains a leaked kubeconfig, a stale service principal secret, or an Azure AD token through phishing, a public endpoint lets them use those credentials from anywhere. With a private cluster they would first need a foothold inside your network, which is a much higher bar.

It exposes you to control plane CVEs

Kubernetes and its supporting components have a steady stream of vulnerabilities. Some have allowed authentication bypass or privilege escalation through the API. When a fresh CVE drops, scanners sweep the internet for exposed API servers within hours. A private endpoint takes you out of that line of fire entirely.

It enables reconnaissance

Even without valid credentials, an exposed API server leaks information. Version banners, supported auth methods, and error message behavior all help an attacker fingerprint your cluster and tailor an attack.

Warning: Many teams assume their cluster is "internal" because workloads sit behind a load balancer. The API server is separate from your application ingress. It can be public even when none of your apps are.

For regulated environments, an internet-facing control plane is also a recurring audit finding. Frameworks like the CIS Azure Kubernetes Service Benchmark and most internal hardening standards expect private API access for production clusters.


How to fix it

There are two levels of remediation. A private cluster is the strong fix. Authorized IP ranges are a lighter-weight compromise when a full private setup is not feasible.

Option 1: Deploy a private cluster (recommended)

Danger: You cannot convert an existing public AKS cluster to a private cluster in place. The private cluster setting is immutable after creation. Switching means creating a new cluster and migrating workloads. Plan this carefully against an existing production cluster.

Create a new private cluster:

az aks create \
  --resource-group my-rg \
  --name my-private-cluster \
  --enable-private-cluster \
  --network-plugin azure \
  --vnet-subnet-id "/subscriptions//resourceGroups/my-rg/providers/Microsoft.Network/virtualNetworks/my-vnet/subnets/aks-subnet" \
  --enable-managed-identity

With a private cluster, the API server is only reachable from inside the VNet (or peered networks). Your engineers and CI runners reach it through one of:

  • A jump host or bastion inside the VNet
  • A VPN or ExpressRoute connection
  • Self-hosted CI/CD agents running in the VNet
  • The AKS command invoke feature for occasional access

That last option is handy for break-glass access without standing up extra infrastructure:

az aks command invoke \
  --resource-group my-rg \
  --name my-private-cluster \
  --command "kubectl get nodes"

Option 2: Restrict the public endpoint with authorized IP ranges

If you must keep a public endpoint, at least constrain who can reach it. Authorized IP ranges allow only specified CIDR blocks to connect, while keeping the FQDN public.

az aks update \
  --resource-group my-rg \
  --name my-cluster \
  --api-server-authorized-ip-ranges "203.0.113.10/32,198.51.100.0/24"

Warning: Authorized IP ranges reduce exposure but do not eliminate it. Source IPs can be spoofed in some scenarios, and office IPs change. Treat this as a stopgap, not a permanent substitute for a private cluster. Also confirm your CI/CD egress IPs are included, or you will lock out your own pipelines.

Terraform example for a private cluster

resource "azurerm_kubernetes_cluster" "this" {
  name                    = "my-private-cluster"
  location                = azurerm_resource_group.this.location
  resource_group_name     = azurerm_resource_group.this.name
  dns_prefix              = "myprivate"
  private_cluster_enabled = true

  default_node_pool {
    name           = "default"
    node_count     = 3
    vm_size        = "Standard_D4s_v5"
    vnet_subnet_id = azurerm_subnet.aks.id
  }

  identity {
    type = "SystemAssigned"
  }

  network_profile {
    network_plugin = "azure"
  }
}

Tip: Pair private_cluster_enabled = true with a private_dns_zone_id set to "System" for Azure-managed DNS, or point it to your own private DNS zone if you run a hub-and-spoke topology and need centralized resolution.


How to prevent it from happening again

Since private cluster mode cannot be retrofitted, catching this before a cluster is created is far cheaper than fixing it after.

Azure Policy

Azure has a built-in policy definition that enforces private clusters. Assign it in audit or deny mode at the subscription or management group scope.

az policy assignment create \
  --name "deny-public-aks" \
  --display-name "AKS clusters must be private" \
  --policy "040732e8-d947-40b8-95d6-854c95024bf8" \
  --scope "/subscriptions/" \
  --params '{"effect":{"value":"Deny"}}'

The policy ID above corresponds to the built-in definition "Azure Kubernetes Service Private Clusters should be enabled". In Deny mode, any attempt to create a public cluster is rejected at deployment time.

Policy-as-code in CI

If you provision with Terraform, gate your pipeline with a checker like Checkov or tfsec. A Checkov run will flag azurerm_kubernetes_cluster resources missing private_cluster_enabled:

checkov -d ./infra --framework terraform \
  --check CKV_AZURE_115

Tip: Run the policy check on pull requests, not just on merge. Catching a public-cluster definition during code review means it never reaches Azure, which saves you a painful migration later.

Continuous monitoring

Policy at deploy time covers new clusters, but drift and out-of-band changes still happen. Lensix continuously evaluates the aks_noprivate check across your subscriptions so a cluster created through a console click or an exception still surfaces as a finding.


Best practices

  • Default to private for production. Treat a public API server as an explicit, justified exception rather than the norm.
  • Plan network access before you go private. Decide up front how engineers and pipelines will reach the API: bastion, VPN, in-VNet agents, or command invoke. A private cluster with no access path frustrates teams and leads to risky workarounds.
  • Layer your defenses. Combine a private cluster with Azure AD integration, Kubernetes RBAC, and network policies. The private endpoint controls who can reach the API, not what they can do once authenticated.
  • Use authorized IP ranges only as a transitional control on clusters that cannot yet be rebuilt as private.
  • Audit the control plane separately from workloads. Application exposure and API server exposure are different problems with different fixes.
  • Document break-glass access. When the API is private, make sure there is a tested, audited path to reach it during an incident so responders are not scrambling.

Locking down the AKS API server is one of the highest-value, lowest-friction hardening steps you can take, provided you do it at creation time. Build it into your cluster templates and policy gates now, and you avoid the migration headache of fixing it later.