Prefer Using Dedicated Amazon EKS Service Accounts
More Info:​
Use IAM roles for service accounts (IRSA) to associate dedicated IAM roles with Kubernetes service accounts, giving pods least-privilege AWS access instead of relying on the shared node role.
Risk Level​
Medium
Address​
Security
Compliance Standards​
- CIS EKS
Triage and Remediation​
- Remediation
Remediation​
Manual Steps
-
Identify pods that use AWS APIs and how they get credentials
- On any machine with AWS CLI access:
aws eks list-clustersaws eks describe-cluster --name <CLUSTER_NAME> --query "cluster.identity.oidc"
- On any machine with
kubectlaccess:kubectl get pods -A -o custom-columns=NS:.metadata.namespace,POD:.metadata.name,SA:.spec.serviceAccountName - For workloads known/suspected to call AWS APIs, inspect their manifests (in Git/IaC) and running pods to see if they:
- Rely on node IAM role (no explicit credentials, no IRSA annotations), or
- Use static credentials (env vars, mounted secrets), or
- Already use IRSA (service account annotated with
eks.amazonaws.com/role-arn).
- On any machine with AWS CLI access:
-
Confirm OIDC provider and IRSA capability for the cluster
- On any machine with AWS CLI access:
aws eks describe-cluster --name <CLUSTER_NAME> --query "cluster.identity.oidc.issuer" --output text
- If this returns
Noneor empty, IRSA is not enabled; plan to create an OIDC provider (via console/CLI/IaC for this cluster) before proceeding with per-service-account changes.
- On any machine with AWS CLI access:
-
Review IAM policies attached to worker node roles for overbroad AWS access
- On any machine with AWS CLI access:
# List nodegroup IAM rolesaws eks list-nodegroups --cluster-name <CLUSTER_NAME>aws eks describe-nodegroup --cluster-name <CLUSTER_NAME> --nodegroup-name <NODEGROUP_NAME> \--query "nodegroup.nodeRole"# Inspect attached policies for each node roleaws iam list-attached-role-policies --role-name <NODE_ROLE_NAME>aws iam list-role-policies --role-name <NODE_ROLE_NAME>
- Examine the policy documents for permissions that should really be scoped to specific applications (for example, broad S3, DynamoDB, KMS, SQS permissions used by only a subset of pods).
- On any machine with AWS CLI access:
-
Design and create least-privilege IAM roles for specific service accounts (IRSA)
- For each application needing AWS access, define a dedicated IAM policy with only required actions and resources. Example (adjust for your case) saved as
app-irsa-policy.json:{"Version": "2012-10-17","Statement": [{"Effect": "Allow","Action": ["s3:GetObject"],"Resource": ["arn:aws:s3:::my-bucket/path/*"]}]} - On any machine with AWS CLI access:
aws iam create-policy --policy-name app-irsa-policy --policy-document file://app-irsa-policy.json
- Create an IAM role for IRSA (trust relationship must reference the cluster’s OIDC issuer and the target service account’s namespace/name). Use console/CLI/IaC to:
- Set the trusted entity to the cluster’s OIDC provider.
- Limit
sts:AssumeRoleWithWebIdentityto the specificsystem:serviceaccount:<NAMESPACE>:<SERVICEACCOUNT_NAME>. - Attach the app’s IAM policy created above.
- For each application needing AWS access, define a dedicated IAM policy with only required actions and resources. Example (adjust for your case) saved as
-
Update Kubernetes service accounts and workloads to use IRSA and remove legacy credential paths
- In Git/IaC (preferred) or via console, for each target workload:
- Create or modify its Kubernetes ServiceAccount to add the IRSA role annotation:
apiVersion: v1kind: ServiceAccountmetadata:name: <SERVICEACCOUNT_NAME>namespace: <NAMESPACE>annotations:eks.amazonaws.com/role-arn: arn:aws:iam::<ACCOUNT_ID>:role/<IRSA_ROLE_NAME>
- Ensure pods use this service account (
spec.serviceAccountName). - Remove any hard-coded AWS access keys (Secrets, env vars, config files) and stop relying on the node role for those permissions.
- Create or modify its Kubernetes ServiceAccount to add the IRSA role annotation:
- After deployment, confirm the annotation is present:
kubectl get sa <SERVICEACCOUNT_NAME> -n <NAMESPACE> -o yaml | grep -A2 "eks.amazonaws.com/role-arn"
- In Git/IaC (preferred) or via console, for each target workload:
-
Reduce overprivileged node IAM permissions and verify IRSA use
- Once all relevant workloads are successfully using IRSA (and functionally tested), on any machine with AWS CLI access:
- Detach or narrow policies from node roles that are no longer needed by all pods:
aws iam detach-role-policy \--role-name <NODE_ROLE_NAME> \--policy-arn arn:aws:iam::<ACCOUNT_ID>:policy/<POLICY_TO_REMOVE>
- Detach or narrow policies from node roles that are no longer needed by all pods:
- Validate credential isolation and least privilege:
- From a pod that should have IRSA, check the AWS identity:
Confirm the ARN matches the dedicated IRSA role.kubectl exec -n <NAMESPACE> <POD_NAME> -- \aws sts get-caller-identity
- From a pod that should not have AWS access, run the same command and confirm it fails or uses a different, appropriate role.
- From a pod that should have IRSA, check the AWS identity:
- Once all relevant workloads are successfully using IRSA (and functionally tested), on any machine with AWS CLI access:
Using kubectl
kubectl can’t configure IAM roles for service accounts or IRSA; this must be implemented in the AWS/IaC layer (EKS cluster, IAM roles, and OIDC provider) using the AWS console, CLI, or infrastructure-as-code. Refer to the Manual Steps section for how to review current usage and migrate workloads to dedicated IRSA-based service accounts.
Automation
#!/usr/bin/env bash
#
# Audit EKS IRSA usage and pods relying on node IAM roles
# Run on: any machine with kubectl access and AWS CLI configured
#
# Requirements:
# - kubectl
# - jq
# - aws (optional but recommended for CloudTrail/IAM checks)
set -euo pipefail
echo "=== 1) Cluster-wide summary of service accounts and IRSA usage ==="
# List all service accounts and whether they have an IAM role annotated
kubectl get sa --all-namespaces -o json \
| jq -r '
[
"NAMESPACE",
"SERVICE_ACCOUNT",
"HAS_IRSA_ROLE",
"IAM_ROLE_ARN"
],
(
.items[]
| {
ns: .metadata.namespace,
name: .metadata.name,
role: (.metadata.annotations["eks.amazonaws.com/role-arn"] // "")
}
| [
.ns,
.name,
(if .role == "" then "no" else "yes" end),
.role
]
)
| @tsv
' \
| column -t
echo
echo "=== 2) Pods that are using a given service account (and whether that SA has IRSA) ==="
# Join pods with their service account and IRSA role annotation
kubectl get pods --all-namespaces -o json \
| jq -r '
[
"NAMESPACE",
"POD",
"SERVICE_ACCOUNT",
"SA_HAS_IRSA_ROLE",
"SA_IAM_ROLE_ARN"
],
(
.items[]
| {
ns: .metadata.namespace,
pod: .metadata.name,
sa: (.spec.serviceAccountName // "default")
}
| . + {
sa_role: (
(input.items[]
| select(.metadata.namespace == .ns and .metadata.name == .sa)
)? # may be null if SA deleted
)
}
# We actually feed SA list via --slurpfile below
)
' 2>/dev/null || true
The above jq is too complex to maintain inline; instead use a safer two-step join:
echo "Collecting service accounts with IRSA annotation..."
SA_TMP="$(mktemp)"
kubectl get sa --all-namespaces -o json \
| jq -r '
.items[]
| [
.metadata.namespace,
.metadata.name,
(.metadata.annotations["eks.amazonaws.com/role-arn"] // "")
]
| @tsv
' > "${SA_TMP}"
echo -e "NAMESPACE\tPOD\tSERVICE_ACCOUNT\tSA_HAS_IRSA_ROLE\tSA_IAM_ROLE_ARN"
kubectl get pods --all-namespaces -o json \
| jq -r '
.items[]
| [
.metadata.namespace,
.metadata.name,
(.spec.serviceAccountName // "default")
]
| @tsv
' \
| while IFS=$'\t' read -r NS POD SA; do
SA_LINE="$(awk -v ns="$NS" -v sa="$SA" '$1 == ns && $2 == sa' "${SA_TMP}" || true)"
SA_ROLE_ARN="$(cut -f3 <<<"$SA_LINE" 2>/dev/null || true)"
if [ -n "$SA_ROLE_ARN" ]; then
HAS_IRSA="yes"
else
HAS_IRSA="no"
fi
printf "%s\t%s\t%s\t%s\t%s\n" "$NS" "$POD" "$SA" "$HAS_IRSA" "$SA_ROLE_ARN"
done \
| column -t
rm -f "${SA_TMP}"
echo
echo "=== 3) Pods that likely rely on node IAM role instead of IRSA ==="
echo "Heuristic: pods whose service account has no eks.amazonaws.com/role-arn annotation."
kubectl get pods --all-namespaces -o json \
| jq -r '
.items[]
| [
.metadata.namespace,
.metadata.name,
(.spec.serviceAccountName // "default")
]
| @tsv
' \
| while IFS=$'\t' read -r NS POD SA; do
ROLE_ANNOTATION="$(
kubectl get sa "$SA" -n "$NS" -o json 2>/dev/null \
| jq -r ".metadata.annotations[\"eks.amazonaws.com/role-arn\"] // \"\""
)"
if [ -z "$ROLE_ANNOTATION" ]; then
printf "%s\t%s\t%s\n" "$NS" "$POD" "$SA"
fi
done \
| {
echo -e "NAMESPACE\tPOD\tSERVICE_ACCOUNT"
column -t
}
echo
echo "=== 4) Optional: identify pods that are likely calling AWS APIs ==="
echo "NOTE: This is heuristic-only, based on common AWS env vars and images."
kubectl get pods --all-namespaces -o json \
| jq -r '
.items[]
| . as $pod
| [
$pod.metadata.namespace,
$pod.metadata.name,
($pod.spec.serviceAccountName // "default"),
(
[
($pod.spec.containers[]?.image // "")
]
| join(",")
),
(
[
($pod.spec.containers[]?.env[]? | select(.name|test("AWS_|AMAZON_")) | .name)
]
| unique
| join(",")
)
]
| @tsv
' \
| {
echo -e "NAMESPACE\tPOD\tSERVICE_ACCOUNT\tIMAGES\tAWS_RELATED_ENV_VARS"
column -t -s $'\t'
}
echo
echo "=== Interpretation guidance ==="
cat <<'EOF'
Findings that likely indicate a problem relative to the benchmark intent:
1) Service accounts with no IRSA role:
- In section (1), any service account with HAS_IRSA_ROLE = "no" that is
used by applications calling AWS APIs is a review candidate.
2) Pods using service accounts without IRSA:
- In section (2) and (3), pods where SA_HAS_IRSA_ROLE = "no" are probably
using the shared node IAM role if they call AWS services, which goes
against the recommendation to prefer dedicated EKS service accounts
with IAM roles (IRSA).
3) Pods likely calling AWS APIs but without IRSA:
- In section (4), look for pods that:
* Have AWS_RELATED_ENV_VARS set (e.g., AWS_REGION, AWS_ACCESS_KEY_ID),
or
* Use images known to interact with AWS (custom app images, tools like
awscli, fluent-bit with S3/CloudWatch outputs, etc.)
and then check whether their service account in earlier sections has
HAS_IRSA_ROLE = "yes".
- If such pods use a service account without an IAM role annotation, they
likely rely on the node IAM role and should be reviewed.
This script only reports current state. Moving workloads from node IAM roles
to IRSA requires:
- Creating IAM roles and policies for each service account,
- Annotating the service accounts with eks.amazonaws.com/role-arn, and
- Ensuring pods use those service accounts.
Those steps must be designed and applied manually (or via IaC) with
least-privilege in mind.
EOF