Find EKS Clusters With a Public API Endpoint

Network cables plugged into a rack-mounted switch

Photo by Lee Lawson on Unsplash

To find EKS clusters with a public endpoint, call ListClusters and DescribeCluster in every Region and read resourcesVpcConfig. A cluster whose endpointPublicAccess is true and whose publicAccessCidrs contains 0.0.0.0/0 accepts Kubernetes API connections from any IP address. That is the default for a new cluster.

Every EKS cluster has a Kubernetes API server endpoint, the URL kubectl, CI pipelines and controllers talk to. When you create a cluster, EKS makes that endpoint public, open to all addresses, and leaves the private endpoint off. Requests still need valid IAM credentials and Kubernetes RBAC permissions, but the API server is reachable by anyone on the internet who wants to try. This example is for platform and security engineers who need to find EKS clusters with a public endpoint across all Regions and decide which ones to lock down.

You’ll get a read-only TypeScript script for the AWS SDK for JavaScript v3 with one row per cluster and a verdict. It changes nothing. Like the other AWS SDK v3 examples for security audits, it’s built to run from a scheduled job as well as by hand.

Which endpoint settings make an EKS cluster public?

Three fields in resourcesVpcConfig decide who can reach the API server. The EKS guide to the cluster API server endpoint documents the combinations:

Public Private Behavior
Enabled Disabled The default. Reachable from the internet (limited by publicAccessCidrs). Node-to-control-plane traffic leaves your VPC, though not Amazon’s network.
Enabled Enabled Reachable from the internet within the CIDRs; traffic from inside the VPC uses the private endpoint, controlled by the cluster security group.
Disabled Enabled No internet access to the API server. kubectl must run inside the VPC or a connected network.

publicAccessCidrs defaults to 0.0.0.0/0, plus ::/0 for dual-stack IPv6 clusters. The CIDRs only affect the public endpoint; the cluster security group only affects the private one. The Kubernetes documentation on controlling access to the API explains the authentication, authorization and admission stages every request passes, which is what still protects a public endpoint. Network restriction adds a layer in front of them.

What does the script report?

  1. Lists RegionsDescribeRegions returns the Regions enabled for your account, or you pass --regions=.
  2. Lists and describes clustersListClusters with the SDK paginator, then DescribeCluster for each name.
  3. Reads the endpoint settingsPublic and private access, the public CIDRs and the authentication mode (API, API_AND_CONFIG_MAP or CONFIG_MAP), which shows whether access entries or the aws-auth ConfigMap map IAM principals to Kubernetes users.
  4. Assigns a verdictOPEN for 0.0.0.0/0 or ::/0, BROAD for an IPv4 range shorter than /16, RESTRICTED for specific CIDRs, and ok for private-only clusters.

Prerequisites

Which IAM permissions does it need?

Replace 123456789012 with your account ID. ListClusters needs "*"; DescribeCluster can be scoped to cluster ARNs.

eks-endpoint-audit-policy.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ListRegionsAndClusters",
      "Effect": "Allow",
      "Action": [
        "ec2:DescribeRegions",
        "eks:ListClusters"
      ],
      "Resource": "*"
    },
    {
      "Sid": "DescribeClusters",
      "Effect": "Allow",
      "Action": "eks:DescribeCluster",
      "Resource": "arn:aws:eks:*:123456789012:cluster/*"
    }
  ]
}

The IAM policy generator for TypeScript AWS SDK code drafts a new policy if you add calls, and the guide to find the IAM actions your AWS SDK for JavaScript code needs explains the mapping.

The script to find EKS clusters with a public endpoint

find-eks-clusters-with-public-endpoint.ts

// find-eks-clusters-with-public-endpoint.ts
// Lists every EKS cluster in each Region with its API server endpoint settings: public access,
// public access CIDRs (flagging 0.0.0.0/0, ::/0 and very broad ranges), private access and the
// authentication mode. Report-only: it never calls UpdateClusterConfig.
// Usage: npx tsx find-eks-clusters-with-public-endpoint.ts [--regions=us-east-1,eu-west-1]
import { EC2Client, DescribeRegionsCommand } from "@aws-sdk/client-ec2";
import { EKSClient, DescribeClusterCommand, paginateListClusters, type Cluster } from "@aws-sdk/client-eks";

const regionArg = process.argv
  .slice(2)
  .find((a) => a.startsWith("--regions="))
  ?.split("=")[1]
  ?.split(",")
  .map((s) => s.trim())
  .filter(Boolean);

interface Row {
  Region: string;
  Cluster: string;
  Version: string;
  Public: boolean;
  Private: boolean;
  PublicCidrs: string;
  AuthMode: string;
  Verdict: string;
}

// An IPv4 CIDR shorter than /16 covers 65,536+ addresses: treat it as broad enough to review.
function isBroad(cidr: string): boolean {
  if (cidr === "0.0.0.0/0" || cidr === "::/0") return true;
  const [ip, bits] = cidr.split("/");
  const len = Number(bits);
  return !!ip && !ip.includes(":") && Number.isFinite(len) && len < 16;
}

function verdict(c: Cluster): string {
  const vpc = c.resourcesVpcConfig;
  const pub = vpc?.endpointPublicAccess === true;
  const priv = vpc?.endpointPrivateAccess === true;
  const cidrs = vpc?.publicAccessCidrs ?? [];
  if (!pub) return priv ? "ok: private endpoint only" : "check: no endpoint access reported";
  if (cidrs.includes("0.0.0.0/0") || cidrs.includes("::/0")) {
    return priv ? "OPEN: public to any IP (private also on)" : "OPEN: public to any IP, nodes use public endpoint";
  }
  if (cidrs.some(isBroad)) return "BROAD: public, a CIDR shorter than /16";
  return priv ? "RESTRICTED: public to listed CIDRs" : "RESTRICTED: public only, nodes must be in the CIDRs";
}

async function listRegions(): Promise<string[]> {
  if (regionArg) return regionArg;
  const out = await new EC2Client({}).send(new DescribeRegionsCommand({}));
  return (out.Regions ?? []).map((r) => r.RegionName ?? "").filter(Boolean).sort();
}

async function main(): Promise<void> {
  const rows: Row[] = [];
  for (const region of await listRegions()) {
    const eks = new EKSClient({ region });
    try {
      for await (const page of paginateListClusters({ client: eks }, {})) {
        for (const name of page.clusters ?? []) {
          const { cluster } = await eks.send(new DescribeClusterCommand({ name }));
          if (!cluster) continue;
          const vpc = cluster.resourcesVpcConfig;
          rows.push({
            Region: region,
            Cluster: name,
            Version: cluster.version ?? "?",
            Public: vpc?.endpointPublicAccess === true,
            Private: vpc?.endpointPrivateAccess === true,
            PublicCidrs: vpc?.endpointPublicAccess ? (vpc.publicAccessCidrs ?? []).join(", ") || "-" : "-",
            AuthMode: cluster.accessConfig?.authenticationMode ?? "?",
            Verdict: verdict(cluster),
          });
        }
      }
    } catch (err) {
      rows.push({ Region: region, Cluster: "?", Version: "?", Public: false, Private: false, PublicCidrs: "?", AuthMode: "?", Verdict: `error: ${err instanceof Error ? err.name : String(err)}` });
    }
  }

  console.table(rows);
  const open = rows.filter((r) => r.Verdict.startsWith("OPEN")).length;
  const broad = rows.filter((r) => r.Verdict.startsWith("BROAD")).length;
  console.log(`${rows.length} cluster(s) checked; ${open} open to any IP, ${broad} with a broad CIDR. Nothing was changed.`);
  if (open || broad) process.exitCode = 2; // lets CI or a scheduled job fail on a finding
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});

How do you run it?

Terminal

npm install @aws-sdk/client-eks @aws-sdk/client-ec2
npm install --save-dev tsx typescript

# Every cluster in every enabled Region
AWS_PROFILE=security-audit npx tsx find-eks-clusters-with-public-endpoint.ts

# Two Regions only
AWS_PROFILE=security-audit npx tsx find-eks-clusters-with-public-endpoint.ts --regions=us-east-1,us-west-2

The script exits with code 2 when any cluster is OPEN or BROAD, so a nightly job can alert on it.

Sample output

Output

┌─────────┬─────────────┬─────────────────┬─────────┬────────┬─────────┬────────────────────────────────────┬──────────────────────┬─────────────────────────────────────────────────────┐
│ (index) │ Region      │ Cluster         │ Version │ Public │ Private │ PublicCidrs                        │ AuthMode             │ Verdict                                             │
├─────────┼─────────────┼─────────────────┼─────────┼────────┼─────────┼────────────────────────────────────┼──────────────────────┼─────────────────────────────────────────────────────┤
│ 0       │ 'eu-west-1' │ 'data-platform' │ '1.32'  │ false  │ true    │ '-'                                │ 'API'                │ 'ok: private endpoint only'                         │
│ 1       │ 'us-east-1' │ 'prod-apps'     │ '1.33'  │ true   │ true    │ '203.0.113.0/24, 198.51.100.17/32' │ 'API_AND_CONFIG_MAP' │ 'RESTRICTED: public to listed CIDRs'                │
│ 2       │ 'us-east-1' │ 'staging'       │ '1.33'  │ true   │ false   │ '0.0.0.0/0'                        │ 'CONFIG_MAP'         │ 'OPEN: public to any IP, nodes use public endpoint' │
│ 3       │ 'us-west-2' │ 'ml-batch'      │ '1.31'  │ true   │ true    │ '10.0.0.0/8'                       │ 'API'                │ 'BROAD: public, a CIDR shorter than /16'            │
└─────────┴─────────────┴─────────────────┴─────────┴────────┴─────────┴────────────────────────────────────┴──────────────────────┴─────────────────────────────────────────────────────┘
4 cluster(s) checked; 1 open to any IP, 1 with a broad CIDR. Nothing was changed.

The names are illustrative. staging is the default configuration: public to everyone, private off, and still on CONFIG_MAP authentication. ml-batch looks restricted, but 10.0.0.0/8 is a private range, so it matches no public client and is probably a mistake for a private endpoint rule. prod-apps is the common middle ground: private on for nodes, public limited to an office range and a CI runner.

How do you restrict or disable public access?

Endpoint access changes go through UpdateClusterConfig. The update is asynchronous, should finish within a few minutes, and the cluster keeps working while its status shows UPDATING. From the AWS CLI:

Terminal

# Turn on the private endpoint and limit the public one to two ranges
aws eks update-cluster-config --region us-east-1 --name staging \
  --resources-vpc-config endpointPrivateAccess=true,endpointPublicAccess=true,publicAccessCidrs="203.0.113.0/24,198.51.100.17/32"

# Track the update with the id from the response
aws eks describe-update --region us-east-1 --name staging --update-id 71abb011-b524-4983-b17f-c30baa1b5530

Check these before you change anything:

  • Nodes need a path. If you restrict the public CIDRs while the private endpoint is off, include the addresses your nodes and Fargate Pods use, or they lose the control plane. Turning on private access first avoids that.
  • The VPC needs DNS. The private endpoint relies on a Route 53 private hosted zone that EKS manages, which needs enableDnsHostnames and enableDnsSupport on the VPC.
  • People and pipelines need access. After public access is off, kubectl works only from the VPC or a connected network (VPN, Transit Gateway, a bastion, or a CloudShell VPC environment). The cluster security group must allow port 443 from those sources.
  • Hybrid nodes. AWS recommends either public or private access, not both, for clusters with EKS Hybrid Nodes.

An exposed API server matters more when the identities behind it are powerful. The scripts to find IAM policies that grant admin access and find unused IAM roles with RoleLastUsed shrink the set of principals that could reach it, and checking that CloudTrail is logging in every Region makes sure calls like UpdateClusterConfig leave a trail.

Troubleshooting

  • AccessDeniedException on DescribeCluster. The Region row shows the error name. Check the policy’s account ID and any SCP; the guide to troubleshoot AWS IAM access denied errors step by step helps.
  • kubectl times out after disabling public access. You’re outside the VPC, DNS resolution is off, or the cluster security group lacks a port 443 rule from your network.
  • The following CIDRs are invalid in publicAccessCidrs. You added an IPv6 range to an IPv4 cluster, or to an IPv6 cluster created before October 2024. Only newer IPv6 clusters accept IPv6 CIDRs.
  • No clusters listed in a Region you use. Check that the Region is enabled for the account and passed in --regions=.

The same exposure question applies to other services: the scripts to find publicly accessible RDS instances, find EC2 instances with public IP addresses and find security groups open to the internet on common ports use the same report style.

Ask ChatWithCloud instead

For a quick look at one Region, ask ChatWithCloud “Which EKS clusters have a public endpoint open to 0.0.0.0/0?” It writes AWS SDK for JavaScript v2 code, runs it locally with your profile and summarizes the JSON result. One caveat from its own limits: it uses SDK v2, so EKS features added after v2’s end of support in September 2025 may be missing. It uses one profile and Region per session and runs code without a confirmation step, so connect ChatWithCloud with a read-only AWS profile. The guide to analyze your AWS security posture with an AI CLI has more questions like this.

Frequently asked questions

Is the EKS API endpoint public by default?

Yes. New clusters have public access enabled with publicAccessCidrs set to 0.0.0.0/0, and private access disabled.

Can anyone access a public EKS endpoint?

Anyone can reach it, but requests still need valid IAM credentials that map to Kubernetes RBAC permissions. Restricting the CIDRs limits who can even attempt a request.

Does changing EKS endpoint access cause downtime?

The cluster keeps functioning during the update. Clients that lose their network path, such as nodes outside the new CIDRs or users outside the VPC, lose access once it completes.

Can I use both public and private endpoints?

Yes. With both enabled, traffic from inside the VPC uses the private endpoint and outside clients use the public endpoint within the allowed CIDRs.

Related guides

Ask your AWS account in plain English

Your first 15 runs are free, with no OpenAI key needed.

npx chatwithcloud