Connecting kubectl to a Private EKS Cluster Over an Internal Domain

Connecting kubectl to a Private EKS Cluster Over an Internal Domain

If you've ever run kubectl get pods against a corporate EKS cluster and hit Unauthorized, a hung connection, or an x509 certificate error, the frustrating part is that the failure could be coming from any of three completely different systems: your AWS identity, your local kubeconfig, or the network path to the API server. This post walks through all three, end to end, with a focus on the case that trips people up most: reaching the cluster through an internal DNS endpoint instead of the public one AWS hands you by default.

The three planes you're actually dealing with

"Connect kubectl to EKS" sounds like one action. It's really the composition of three independent control planes, and each has its own way to fail:

  1. Identity plane (AWS IAM) --- who you are, in AWS's eyes.
  2. Cluster access plane (kubeconfig + exec auth) --- how a Kubernetes-shaped credential gets minted from that AWS identity.
  3. Network and trust plane (DNS, routing, TLS) --- how your packets physically reach the API server, and how you verify you're actually talking to it.

A green result on one plane tells you nothing about the others. A successful aws sts get-caller-identity does not mean kubectl get pods will work --- that's a completely separate authorization step inside Kubernetes.
#mermaid-svg-vAirXzZsGCrx7HB6{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vAirXzZsGCrx7HB6 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vAirXzZsGCrx7HB6 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vAirXzZsGCrx7HB6 .error-icon{fill:#552222;}#mermaid-svg-vAirXzZsGCrx7HB6 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vAirXzZsGCrx7HB6 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vAirXzZsGCrx7HB6 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vAirXzZsGCrx7HB6 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vAirXzZsGCrx7HB6 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vAirXzZsGCrx7HB6 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vAirXzZsGCrx7HB6 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vAirXzZsGCrx7HB6 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vAirXzZsGCrx7HB6 .marker.cross{stroke:#333333;}#mermaid-svg-vAirXzZsGCrx7HB6 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vAirXzZsGCrx7HB6 p{margin:0;}#mermaid-svg-vAirXzZsGCrx7HB6 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-vAirXzZsGCrx7HB6 .cluster-label text{fill:#333;}#mermaid-svg-vAirXzZsGCrx7HB6 .cluster-label span{color:#333;}#mermaid-svg-vAirXzZsGCrx7HB6 .cluster-label span p{background-color:transparent;}#mermaid-svg-vAirXzZsGCrx7HB6 .label text,#mermaid-svg-vAirXzZsGCrx7HB6 span{fill:#333;color:#333;}#mermaid-svg-vAirXzZsGCrx7HB6 .node rect,#mermaid-svg-vAirXzZsGCrx7HB6 .node circle,#mermaid-svg-vAirXzZsGCrx7HB6 .node ellipse,#mermaid-svg-vAirXzZsGCrx7HB6 .node polygon,#mermaid-svg-vAirXzZsGCrx7HB6 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vAirXzZsGCrx7HB6 .rough-node .label text,#mermaid-svg-vAirXzZsGCrx7HB6 .node .label text,#mermaid-svg-vAirXzZsGCrx7HB6 .image-shape .label,#mermaid-svg-vAirXzZsGCrx7HB6 .icon-shape .label{text-anchor:middle;}#mermaid-svg-vAirXzZsGCrx7HB6 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-vAirXzZsGCrx7HB6 .rough-node .label,#mermaid-svg-vAirXzZsGCrx7HB6 .node .label,#mermaid-svg-vAirXzZsGCrx7HB6 .image-shape .label,#mermaid-svg-vAirXzZsGCrx7HB6 .icon-shape .label{text-align:center;}#mermaid-svg-vAirXzZsGCrx7HB6 .node.clickable{cursor:pointer;}#mermaid-svg-vAirXzZsGCrx7HB6 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-vAirXzZsGCrx7HB6 .arrowheadPath{fill:#333333;}#mermaid-svg-vAirXzZsGCrx7HB6 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-vAirXzZsGCrx7HB6 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-vAirXzZsGCrx7HB6 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vAirXzZsGCrx7HB6 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-vAirXzZsGCrx7HB6 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vAirXzZsGCrx7HB6 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-vAirXzZsGCrx7HB6 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-vAirXzZsGCrx7HB6 .cluster text{fill:#333;}#mermaid-svg-vAirXzZsGCrx7HB6 .cluster span{color:#333;}#mermaid-svg-vAirXzZsGCrx7HB6 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-vAirXzZsGCrx7HB6 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-vAirXzZsGCrx7HB6 rect.text{fill:none;stroke-width:0;}#mermaid-svg-vAirXzZsGCrx7HB6 .icon-shape,#mermaid-svg-vAirXzZsGCrx7HB6 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vAirXzZsGCrx7HB6 .icon-shape p,#mermaid-svg-vAirXzZsGCrx7HB6 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-vAirXzZsGCrx7HB6 .icon-shape .label rect,#mermaid-svg-vAirXzZsGCrx7HB6 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vAirXzZsGCrx7HB6 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-vAirXzZsGCrx7HB6 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-vAirXzZsGCrx7HB6 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Enterprise SSO / IdP
Assume IAM role
aws eks get-token via kubeconfig exec
HTTPS to EKS endpoint
IAM auth on control plane
Kubernetes RBAC authorization

If any stage in this chain fails, kubectl fails. The rest of this post sets up the first four stages so the last two have a chance to succeed.

Why an internal domain at all?

By default, aws eks update-kubeconfig points your cluster entry at the public EKS API endpoint AWS publishes for the cluster. That's fine for a personal sandbox. Inside an enterprise network it's often deliberately unreachable or undesired: private EKS endpoints, split-horizon DNS, internal load balancers, or a proxy/ingress in front of the control plane are all common reasons your platform team hands you an internal hostname instead --- something like k8s.<cluster>.internal.example.com --- and expects you to point your kubeconfig at that instead of the AWS-issued endpoint.

That rewrite is simple mechanically, but it changes two things at once: the route packets take, and the certificate your client needs to trust. Getting the first right and skipping the second is the most common footgun in this whole flow --- more on that below.

Prerequisites

  • An enterprise identity that's federated into AWS (SAML via your corporate IdP, or another federation method) with a role that has EKS access.
  • The AWS CLI and kubectl installed locally.
  • The internal DNS hostname for the cluster's API server, and --- ideally --- the internal CA certificate that signs it, from your platform/networking team.

Step 1 --- Establish AWS identity (identity plane)

Everything downstream depends on you holding a valid, short-lived AWS credential. In a federated enterprise setup this typically means exchanging an SSO session for STS credentials --- see assuming-an-aws-iam-role-via-federation-sts.md for the full mechanics of that exchange. Conceptually:

bash 复制代码
# however your organization federates --- SAML, OIDC, or a wrapper script around it
assume-role <account-id> --role <role-name>

Verify it worked before moving on:

bash 复制代码
aws sts get-caller-identity

If this fails, nothing past this point can succeed --- aws eks get-token has no identity to mint a Kubernetes token from.

Step 2 --- Wire up kubeconfig (cluster access plane)

bash 复制代码
aws eks update-kubeconfig --name <cluster-name> --region <region>

This isn't just a convenience alias --- it writes three things into ~/.kube/config:

  • clusters --- the API endpoint URL and certificate authority data.
  • users --- an exec-based auth plugin entry that runs aws eks get-token on demand, rather than storing a static token.
  • contexts --- binding a specific user entry to a specific cluster entry.

The generated user entry looks roughly like this:

yaml 复制代码
users:
- name: arn:aws:eks:<region>:<account-id>:cluster/<cluster-name>
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1beta1
      command: aws
      args:
      - eks
      - get-token
      - --cluster-name
      - <cluster-name>
      - --region
      - <region>

Here's the part that surprises people: kubectl does not carry a static bearer token around. Every time it needs to talk to the API server, it re-executes aws eks get-token, which returns a short-lived, wrapped GetCallerIdentity request signed with your current AWS credentials:

json 复制代码
{
  "kind": "ExecCredential",
  "apiVersion": "client.authentication.k8s.io/v1beta1",
  "spec": {},
  "status": {
    "expirationTimestamp": "2026-07-19T12:34:56Z",
    "token": "k8s-aws-v1.<base64url-encoded-presigned-sts-call>"
  }
}

The EKS authentication webhook validates that signed proof and extracts your AWS identity from it. That identity still has to be mapped to Kubernetes RBAC separately (via aws-auth / access entries) --- token validity alone grants nothing.

Step 3 --- Point the cluster entry at the internal endpoint (network/trust plane)

This is the step unique to internal-domain access. Your platform team's internal DNS entry needs to replace the default public endpoint in the clusters section of your kubeconfig.

The quick-and-dirty version, which you'll see in a lot of tribal-knowledge runbooks:

bash 复制代码
kubectl config set-cluster arn:aws:eks:<region>:<account-id>:cluster/<cluster-name> \
  --server=https://k8s.<cluster-name>.internal.example.com \
  --insecure-skip-tls-verify=true

This works, and that's exactly the problem. --insecure-skip-tls-verify=true disables certificate validation entirely --- your client will happily hand its bearer token to anything answering on that hostname, with zero guarantee it's actually your cluster's API server. Inside a trusted internal network the practical risk is lower than it sounds, but it's still throwing away the one mechanism that proves endpoint authenticity, and it's easy to forget it's set until something goes wrong.

The version worth actually using:

bash 复制代码
kubectl config set-cluster arn:aws:eks:<region>:<account-id>:cluster/<cluster-name> \
  --server=https://k8s.<cluster-name>.internal.example.com \
  --certificate-authority=/path/to/internal-ca.pem \
  --embed-certs=true

Ask your platform/networking team for the internal CA certificate that signs the internal endpoint (or the internal load balancer/ingress in front of it). Pointing --certificate-authority at it keeps TLS verification intact --- you get the internal routing you need without giving up endpoint authenticity. Treat --insecure-skip-tls-verify as a debugging-only escape hatch, never a permanent config.

Verifying the whole chain

bash 复制代码
aws sts get-caller-identity        # confirms identity plane only
kubectl get pods -n <namespace>    # exercises all three planes end-to-end

Only the second command actually proves the full path works --- DNS resolves, TLS trusts the endpoint, the token mints correctly, and your IAM identity is mapped to Kubernetes RBAC.

What happens on a real request

EKS Kubernetes API (internal endpoint) AWS STS/IAM exec plugin (aws eks get-token) kubectl User Shell EKS Kubernetes API (internal endpoint) AWS STS/IAM exec plugin (aws eks get-token) kubectl User Shell #mermaid-svg-gwl9ACEMaGXS8mzh{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gwl9ACEMaGXS8mzh .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gwl9ACEMaGXS8mzh .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gwl9ACEMaGXS8mzh .error-icon{fill:#552222;}#mermaid-svg-gwl9ACEMaGXS8mzh .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gwl9ACEMaGXS8mzh .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gwl9ACEMaGXS8mzh .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gwl9ACEMaGXS8mzh .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gwl9ACEMaGXS8mzh .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gwl9ACEMaGXS8mzh .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gwl9ACEMaGXS8mzh .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gwl9ACEMaGXS8mzh .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gwl9ACEMaGXS8mzh .marker.cross{stroke:#333333;}#mermaid-svg-gwl9ACEMaGXS8mzh svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gwl9ACEMaGXS8mzh p{margin:0;}#mermaid-svg-gwl9ACEMaGXS8mzh .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-gwl9ACEMaGXS8mzh text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-gwl9ACEMaGXS8mzh .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-gwl9ACEMaGXS8mzh .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-gwl9ACEMaGXS8mzh .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-gwl9ACEMaGXS8mzh .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-gwl9ACEMaGXS8mzh #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-gwl9ACEMaGXS8mzh .sequenceNumber{fill:white;}#mermaid-svg-gwl9ACEMaGXS8mzh #sequencenumber{fill:#333;}#mermaid-svg-gwl9ACEMaGXS8mzh #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-gwl9ACEMaGXS8mzh .messageText{fill:#333;stroke:none;}#mermaid-svg-gwl9ACEMaGXS8mzh .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-gwl9ACEMaGXS8mzh .labelText,#mermaid-svg-gwl9ACEMaGXS8mzh .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-gwl9ACEMaGXS8mzh .loopText,#mermaid-svg-gwl9ACEMaGXS8mzh .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-gwl9ACEMaGXS8mzh .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-gwl9ACEMaGXS8mzh .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-gwl9ACEMaGXS8mzh .noteText,#mermaid-svg-gwl9ACEMaGXS8mzh .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-gwl9ACEMaGXS8mzh .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-gwl9ACEMaGXS8mzh .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-gwl9ACEMaGXS8mzh .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-gwl9ACEMaGXS8mzh .actorPopupMenu{position:absolute;}#mermaid-svg-gwl9ACEMaGXS8mzh .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-gwl9ACEMaGXS8mzh .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-gwl9ACEMaGXS8mzh .actor-man circle,#mermaid-svg-gwl9ACEMaGXS8mzh line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-gwl9ACEMaGXS8mzh :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} kubectl get pods -n <namespace> 1 Need credentials for active context 2 Use current role session to prove AWS identity 3 Signed identity proof / token material 4 ExecCredential bearer token (short-lived) 5 HTTPS request over internal DNS + Authorization: Bearer <token> 6 Authenticate IAM identity 7 Authorize via aws-auth/access entries + RBAC 8 Kubernetes API response 9

Caption: identity is established once (step 1--2); after that, every kubectl call independently re-derives a fresh token (step 3 in the diagram) and sends it over the internal route configured in step 3 above.

Reading the failure modes

Because three independent planes are involved, the error message usually tells you which one broke:

Symptom Likely plane What to check
You must be logged in to the server (Unauthorized) Authentication/authorization Token minting, IAM role, aws-auth/access-entry mapping, RBAC bindings
Unable to connect to the server Network DNS resolution of the internal hostname, VPN/routing, firewall/security groups, private endpoint access
x509 / certificate errors TLS trust Whether --certificate-authority matches what the internal endpoint actually presents; a stale or wrong internal CA is the usual culprit
get-caller-identity succeeds, kubectl still fails Authorization Your AWS identity was never mapped into the cluster's RBAC --- this is a Kubernetes-side configuration, not an AWS one

The three-question mental model

Keep these separate --- collapsing them is where most confusion comes from:

  1. Identity --- "Who are you in AWS?" Established by your federated role assumption.
  2. Authentication --- "Can you prove that identity to the EKS API?" Performed by aws eks get-token, invoked automatically by the kubeconfig exec plugin.
  3. Authorization --- "What are you allowed to do inside Kubernetes?" Controlled entirely by IAM-to-Kubernetes RBAC mapping, independent of whether authentication succeeded.

A cluster reachable over an internal domain doesn't change this model --- it just adds a fourth, purely mechanical question underneath all three: can your packets and your TLS handshake actually get there? Get that one right with a trusted CA instead of a bypassed check, and the rest of the chain behaves exactly like connecting to any other EKS cluster.

相关推荐
Geek-Chow18 小时前
A Practical Tour of AWS Networking: VPCs, Subnets, Gateways, and More
网络·aws
运维大师1 天前
【K8S 运维实战】03-网络模型实战CNI
运维·网络·kubernetes
运维大师1 天前
【K8S 运维实战】04-存储体系梳理CSI
运维·容器·kubernetes
heimeiyingwang1 天前
【架构实战】GitOps实践:Kubernetes上的声明式交付
elasticsearch·架构·kubernetes
Ai拆代码的曹操2 天前
K8s Pod Pending 逐层排查:从 FailedScheduling 到 PVC StorageClass 不存在的实战记录
java·linux·kubernetes
运维大师2 天前
【K8S 运维实战】01-K8s架构深度剖析
运维·架构·kubernetes
Geek-Chow2 天前
Assuming an AWS IAM Role via Federation
云计算·aws
spider_xcxc2 天前
docker-compose.yaml 是“开发/测试环境”的利器,而 Kubernetes 配置是“生产环境”的标准。
docker·容器·kubernetes
chen_ke_hao2 天前
K8s 高可用集群部署
java·docker·kubernetes