Reference for the full path from "I have base AWS credentials" to "kubectl works against an EKS cluster as an assumed role": aws sts assume-role, aws eks update-kubeconfig, and validating with kubectl.
Assumes AWS CLI v2 (the aws eks get-token exec plugin is built in — no separate aws-iam-authenticator binary needed) and kubectl installed. Append --region <r> / --profile <p> to scope any command.
There is no password and no static kubeconfig token. Each kubectl call mints a short-lived token on the fly:
kubectl runs the exec credential plugin named in the kubeconfig user block — aws eks get-token.GetCallerIdentity URL, encoded as a bearer token (valid ~15 min).aws-auth ConfigMap (legacy).Two distinct authorization layers, and both must allow you:
| Layer | Question | Where it's configured |
|---|---|---|
| AWS IAM | Can this principal call eks:DescribeCluster / assume the role? |
IAM policies + trust policy |
| K8s authn → authz | Does the cluster recognize my IAM ARN, and does RBAC permit the verb? | access entries / aws-auth + RBAC |
Key consequence: the kubeconfig stores which IAM identity to use, not a credential. Change your ambient AWS creds (or the --role-arn baked into the kubeconfig) and the same kubeconfig authenticates as someone else. This is why "assume the role" and "update the kubeconfig" are two separate steps you can mix and match.
Always start here — most EKS auth confusion is "I'm not the IAM identity I think I am."
aws sts get-caller-identity
{
"UserId": "AROA...:session-name",
"Account": "111122223333",
"Arn": "arn:aws:sts::111122223333:assumed-role/Admin/session-name"
}
Arn with assumed-role/... → you're already in an assumed-role session.Arn with user/... → you're a long-term IAM user.arn:aws:iam::...:role/Admin, not the session ARN assumed-role/Admin/sess).aws eks list-clusters
aws eks describe-cluster --name my-cluster \
--query 'cluster.{endpoint:endpoint,status:status,version:version,arn:arn}' \
--output yaml
Useful one-offs:
| Command | Purpose |
|---|---|
| aws eks describe-cluster --name C --query 'cluster.status' | Should be ACTIVE before you bother |
| aws eks describe-cluster --name C --query 'cluster.accessConfig.authenticationMode' | API, API_AND_CONFIG_MAP, or CONFIG_MAP — tells you which auth backend is live (§7) |
| aws eks describe-cluster --name C --query 'cluster.resourcesVpcConfig.endpointPublicAccess' | Is the API reachable from where you are? |
Three ways, pick by use case.
assume-role + export env varsCREDS=$(aws sts assume-role \
--role-arn arn:aws:iam::111122223333:role/EKSAdmin \
--role-session-name "$(whoami)-eks-$(date +%s)" \
--duration-seconds 3600 \
--output json)
export AWS_ACCESS_KEY_ID=$(jq -r '.Credentials.AccessKeyId' <<<"$CREDS")
export AWS_SECRET_ACCESS_KEY=$(jq -r '.Credentials.SecretAccessKey' <<<"$CREDS")
export AWS_SESSION_TOKEN=$(jq -r '.Credentials.SessionToken' <<<"$CREDS")
aws sts get-caller-identity # confirm you're now the assumed role
To revert, unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN (falls back to your base profile).
With MFA:
aws sts assume-role --role-arn arn:aws:iam::...:role/EKSAdmin \
--role-session-name s1 \
--serial-number arn:aws:iam::111122223333:mfa/alice \
--token-code 123456
role_arn (recommended)In ~/.aws/config — the SDK assumes the role automatically and caches/refreshes the session for you:
[profile eks-admin]
role_arn = arn:aws:iam::111122223333:role/EKSAdmin
source_profile = default # base creds that can assume the role
# mfa_serial = arn:aws:iam::111122223333:mfa/alice # prompts on demand
region = us-east-1
Then everything is just --profile eks-admin:
aws sts get-caller-identity --profile eks-admin
SSO variant:
[profile eks-admin]
sso_session = my-sso
sso_account_id = 111122223333
sso_role_name = EKSAdmin
region = us-east-1
aws sso login --profile eks-admin
aws eks update-kubeconfig --role-arn ... (§4) bakes the assume-role into the exec plugin, so the role is assumed per kubectl call from your base creds. Cleanest for "this kubeconfig context is always this role." See §4b.
aws eks update-kubeconfig writes (or merges into) ~/.kube/config a cluster + context + exec-plugin user block.
aws eks update-kubeconfig --name my-cluster --region us-east-1
Adds a context like arn:aws:eks:us-east-1:111122223333:cluster/my-cluster and makes it current. The user block runs aws eks get-token --cluster-name my-cluster with no pinned profile/role, so it uses whatever creds the shell has (e.g. the env vars from §3a, or AWS_PROFILE).
aws eks update-kubeconfig --name my-cluster --region us-east-1 \
--role-arn arn:aws:iam::111122223333:role/EKSAdmin \
--alias eks-admin
Now the exec block carries --role-arn ...; every kubectl assumes that role from your base creds. --alias sets a friendly context name instead of the long ARN.
aws eks update-kubeconfig --name my-cluster --profile eks-admin --alias eks-admin
Injects env: [{name: AWS_PROFILE, value: eks-admin}] into the user's exec block, so kubectl always uses that profile regardless of the ambient AWS_PROFILE. Most robust for day-to-day — the context is self-contained.
| Flag | Effect |
|---|---|
--alias NAME |
Context (and cluster/user) name in kubeconfig — avoid the giant ARN default |
--role-arn ARN |
Bake an assume-role into the exec plugin (§4b) |
--profile NAME |
Bake AWS_PROFILE into the exec plugin (§4c) |
--kubeconfig PATH |
Write somewhere other than ~/.kube/config (good for isolation: --kubeconfig /tmp/eks.conf) |
--user-alias NAME |
Custom user-entry name (handy when merging many clusters) |
--dry-run |
Print the YAML it would write, change nothing — inspect before merging |
What a generated user block looks like:
users:
- name: eks-admin
user:
exec:
apiVersion: client.authentication.k8s.io/v1beta1
command: aws
args: [eks, get-token, --cluster-name, my-cluster, --output, json]
# if --role-arn was used: ..., --role-arn, arn:aws:iam::...:role/EKSAdmin
env:
- {name: AWS_PROFILE, value: eks-admin} # if --profile was used
Run these top-to-bottom; each rules out a layer.
# 0. Which context am I on?
kubectl config current-context
# 1. Does the exec plugin mint a token at all? (pure AWS-side check, no API call)
aws eks get-token --cluster-name my-cluster >/dev/null && echo "token OK"
# 2. Can I reach the API server and is authn working?
kubectl version # prints client + server versions on success
kubectl get --raw='/readyz' # 'ok' if the API server is reachable & healthy
# 3. Authn vs authz: who does the cluster think I am, and what may I do?
kubectl auth whoami # shows the mapped K8s username/groups (k8s 1.26+)
kubectl auth can-i get pods --all-namespaces
kubectl auth can-i '*' '*' --all-namespaces # am I effectively cluster-admin?
# 4. The real smoke test
kubectl get nodes
kubectl get ns
Cross-check that kubectl and the AWS CLI agree on identity:
aws sts get-caller-identity --query Arn --output text
# ...should be the IAM role you expect to be RBAC-mapped in the cluster
kubectl auth can-i --list dumps every verb/resource you're allowed — fastest way to see your effective RBAC.
# What the exec plugin returns to kubectl (an ExecCredential with a ~15-min token)
aws eks get-token --cluster-name my-cluster --output json | jq .
# Assume a role *inside* the token call (same as kubeconfig --role-arn)
aws eks get-token --cluster-name my-cluster \
--role-arn arn:aws:iam::111122223333:role/EKSAdmin
The token is a base64url-encoded pre-signed STS URL (k8s-aws-v1.<blob>). It is not a JWT and carries no scopes — all authorization happens cluster-side after STS resolves the ARN. Tokens are short-lived by design; there's nothing to "refresh" or store.
If authn succeeds but you get Forbidden, or you get Unauthorized/the server has asked for the client to provide credentials, the IAM→K8s mapping is the culprit. Two mechanisms:
# List who's mapped
aws eks list-access-entries --cluster-name my-cluster
# Map an IAM role and grant cluster-admin via an access policy
aws eks create-access-entry --cluster-name my-cluster \
--principal-arn arn:aws:iam::111122223333:role/EKSAdmin
aws eks associate-access-policy --cluster-name my-cluster \
--principal-arn arn:aws:iam::111122223333:role/EKSAdmin \
--access-scope type=cluster \
--policy-arn arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy
Requires the cluster's authenticationMode to be API or API_AND_CONFIG_MAP (§2). No in-cluster kubectl needed to grant access — it's all AWS API.
aws-auth ConfigMap (legacy, in-cluster)kubectl -n kube-system get configmap aws-auth -o yaml
Maps role/user ARNs to K8s usernames + groups:
data:
mapRoles: |
- rolearn: arn:aws:iam::111122223333:role/EKSAdmin
username: admin
groups: [system:masters] # system:masters == cluster-admin
Editing it requires you to already have access — chicken-and-egg if you lock yourself out. The cluster creator IAM identity always has implicit system:masters; keep one such identity around as a break-glass.
Match the ARN exactly: map the role ARN (:role/EKSAdmin), not the assumed-role session ARN. Don't include a path-stripped or sts-flavored variant.
# Assume
CREDS=$(aws sts assume-role --role-arn arn:aws:iam::111122223333:role/EKSAdmin \
--role-session-name eks --output json)
export AWS_ACCESS_KEY_ID=$(jq -r .Credentials.AccessKeyId <<<"$CREDS")
export AWS_SECRET_ACCESS_KEY=$(jq -r .Credentials.SecretAccessKey <<<"$CREDS")
export AWS_SESSION_TOKEN=$(jq -r .Credentials.SessionToken <<<"$CREDS")
# Wire up kubeconfig (uses the env creds above)
aws eks update-kubeconfig --name my-cluster --region us-east-1 --alias eks-tmp
# Validate
aws sts get-caller-identity --query Arn --output text
kubectl auth can-i get pods && kubectl get nodes
# ~/.aws/config has [profile eks-admin] with role_arn + source_profile
aws eks update-kubeconfig --name my-cluster --region us-east-1 \
--profile eks-admin --alias eks-admin
kubectl config use-context eks-admin
kubectl get nodes # assumes the role per-call via the baked-in AWS_PROFILE
export KUBECONFIG=/tmp/eks-$$.conf
aws eks update-kubeconfig --name my-cluster --region us-east-1 \
--role-arn arn:aws:iam::111122223333:role/CIDeployer
kubectl apply -f manifests/
rm -f "$KUBECONFIG"; unset KUBECONFIG
The role in acct B must trust acct A in its trust policy; then either pre-assume (§3a) or bake --role-arn (the acct-B role) into the kubeconfig (§4b). The acct-B role must also be mapped via access entries / aws-auth in the acct-B cluster.
| Symptom | Likely cause | Fix |
|---|---|---|
error: You must be logged in to the server (Unauthorized) |
IAM ARN not mapped in cluster, or ambient creds aren't who you think | aws sts get-caller-identity; then check §7 access entries / aws-auth maps that exact role ARN |
Error: ... is not authorized to perform: sts:AssumeRole |
Trust policy on the target role doesn't allow your principal | Edit the role's trust relationship to allow your user/role ARN |
error loading credentials / Unable to locate credentials from aws eks get-token |
The exec plugin has no creds at kubectl time (env vars expired, wrong/empty AWS_PROFILE) |
Re-assume (§3a) or pin a profile into the kubeconfig (§4c) |
... Forbidden on a specific verb |
Authn works, RBAC doesn't grant it | kubectl auth can-i --list; bind the needed Role/ClusterRole, or use a broader access policy (§7a) |
the server has asked for the client to provide credentials |
Token minted but cluster rejected the identity (ARN mismatch, e.g. mapped session ARN instead of role ARN) | Map the role ARN, not assumed-role/.../session (§7) |
You must specify a region |
No region on the cluster/command | --region or set AWS_REGION / profile region |
| Works in one shell, not another | AWS_PROFILE / env-var creds differ between shells |
Pin the profile into the kubeconfig (§4c) so it's context-local, not shell-local |
| Token expired mid-long-command | STS session shorter than the operation | Use a profile/role_arn (auto-refresh) instead of static env-var creds; raise --duration-seconds (capped by the role's max session duration) |
An error occurred (ExpiredTokenException) |
Assumed-role session lapsed | Re-assume; for SSO, aws sso login again |
Quick triage order: aws sts get-caller-identity (am I who I think?) → aws eks get-token (do creds mint a token?) → kubectl auth whoami (does the cluster recognize me?) → kubectl auth can-i ... (does RBAC allow the verb?).
--alias always. The default context name is the cluster ARN — unwieldy and easy to fat-finger. Alias everything.--profile baked in beats env-var juggling. A --profile-pinned kubeconfig (§4c) is reproducible across shells and survives unset; env-var creds (§3a) are ephemeral and shell-local.:role/Name, not the assumed-role/Name/session form, and mind any IAM path (:role/team/Name).aws-auth on clusters that support it (authenticationMode API/API_AND_CONFIG_MAP): API-managed, auditable, and you can't brick the cluster by botching a ConfigMap edit.system:masters; never delete it, and know which identity it is before you start tightening RBAC.get-token works without a cluster round-trip — if it fails, the problem is purely AWS-side (creds/region), not Kubernetes. Great isolation step.eks:DescribeCluster" (IAM) says nothing about "I can kubectl get pods" (RBAC). Check both.KUBECONFIG=/tmp/foo for isolation. Don't pollute ~/.kube/config for one-off or CI access; point KUBECONFIG at a temp file and delete it after.kubectl wait/logs -f/port-forward sessions outlive the token but the exec plugin re-mints transparently — as long as the underlying AWS creds are still valid. Static env-var creds that expire mid-stream will break it; auto-refreshing profiles won't.