Skip to main content

Prefer Using Dedicated Amazon Eks Service Accounts

More Info:

Kubernetes workloads should not use cluster node service accounts to authenticate to Amazon EKS APIs. Each Kubernetes workload that needs to authenticate to other AWS services using AWS IAM should be provisioned with a dedicated service account.

Risk Level

High

Address

Security

Compliance Standards

  • APRA CPS 234 (Australia)
  • BSI C5 (Germany)
  • Brazil LGPD
  • CCPA / CPRA (California)
  • CIS Critical Security Controls v8
  • CIS EKS
  • CMMC 2.0
  • CSA Cloud Controls Matrix v4
  • DPDPA
  • Digital Operational Resilience Act (EU)
  • Essential 8
  • ISO/IEC 27017
  • ISO/IEC 27018
  • ISO/IEC 27701
  • KSA PDPL
  • MAS Technology Risk Management (Singapore)
  • MITRE ATT&CK (Cloud)
  • NIST SP 800-171
  • NYDFS 23 NYCRR 500
  • SWIFT Customer Security Controls Framework
  • Sarbanes-Oxley IT General Controls
  • UK NCSC Cyber Assessment Framework

Triage and Remediation

Remediation

Manual Steps
  1. Identify pods using node IAM role credentials

    • On any machine with kubectl access, list all namespaces and pods that appear to use the EC2 instance role (for example via environment variables or mounted credential files):
      kubectl get ns
      kubectl get pods -A -o wide
      kubectl get pods -A -o yaml | grep -nE '169\.254\.169\.254|AWS_ACCESS_KEY_ID|AWS_SECRET_ACCESS_KEY|AWS_SESSION_TOKEN'
    • In the AWS console, review the IAM policies attached to your EKS worker node instance profile; if they include broad permissions for application services (S3, DynamoDB, SQS, etc.), those workloads are likely relying on node roles.
  2. Confirm IRSA (IAM Roles for Service Accounts) is enabled for the cluster

    • On any machine with AWS CLI configured, check for the OIDC provider associated with your EKS cluster (replace the cluster name and region):
      aws eks describe-cluster \
      --name my-eks-cluster \
      --region us-east-1 \
      --query "cluster.identity.oidc.issuer" \
      --output text
    • If this returns None or an empty value, create an IAM OIDC provider for the cluster using the AWS console or:
      eksctl utils associate-iam-oidc-provider \
      --cluster=my-eks-cluster \
      --region=us-east-1 \
      --approve
  3. Select one workload that currently uses node credentials and analyze its access needs

    • On any machine with kubectl access, inspect a representative pod:
      kubectl -n my-namespace get pod my-pod -o yaml
    • Determine what AWS services the application actually needs (for example from code, documentation, or environment variables like S3_BUCKET, QUEUE_URL).
    • Document the minimal IAM permissions required (actions and resources) for this workload.
  4. Create a dedicated IAM role and Kubernetes service account for that workload (IRSA)

    • On any machine with AWS CLI configured, create an IAM role that trusts the EKS OIDC provider and your chosen service account (substitute the actual OIDC URL, namespace, and service account name):
      OIDC_URL="$(aws eks describe-cluster \
      --name my-eks-cluster \
      --region us-east-1 \
      --query "cluster.identity.oidc.issuer" \
      --output text)"
      ACCOUNT_ID="$(aws sts get-caller-identity --query Account --output text)"

      cat > trust-policy.json << 'EOF'
      {
      "Version": "2012-10-17",
      "Statement": [
      {
      "Effect": "Allow",
      "Principal": {
      "Federated": "arn:aws:iam::ACCOUNT_ID:oidc-provider/OIDC_HOST"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
      "StringEquals": {
      "OIDC_HOST:sub": "system:serviceaccount:my-namespace:my-sa"
      }
      }
      }
      ]
      }
      EOF

      # Replace ACCOUNT_ID and OIDC_HOST placeholders
      sed -i "s/ACCOUNT_ID/${ACCOUNT_ID}/g" trust-policy.json
      sed -i "s/OIDC_HOST/${OIDC_URL#https:\/\/}/g" trust-policy.json

      aws iam create-role \
      --role-name my-workload-role \
      --assume-role-policy-document file://trust-policy.json

      # Attach least-privilege policy (use a custom policy ARN you’ve created)
      aws iam attach-role-policy \
      --role-name my-workload-role \
      --policy-arn arn:aws:iam::${ACCOUNT_ID}:policy/my-workload-policy
    • On any machine with kubectl access, create/annotate the Kubernetes service account to use this role:
      kubectl create namespace my-namespace --dry-run=client -o yaml | kubectl apply -f -

      kubectl apply -f - << 'EOF'
      apiVersion: v1
      kind: ServiceAccount
      metadata:
      name: my-sa
      namespace: my-namespace
      annotations:
      eks.amazonaws.com/role-arn: arn:aws:iam::ACCOUNT_ID:role/my-workload-role
      EOF
      Replace ACCOUNT_ID with your actual account ID.
  5. Migrate the workload to use the dedicated service account instead of node credentials

    • On any machine with kubectl access, patch or update the workload spec to reference the new service account and remove explicit long-term credentials or metadata-URL usage from the pod spec:
      kubectl -n my-namespace patch deployment my-deployment \
      -p '{"spec":{"template":{"spec":{"serviceAccountName":"my-sa"}}}}'
    • Ensure application code is using standard AWS SDK credential resolution (no hard-coded keys, no direct calls to 169.254.169.254) so it automatically uses the IRSA-provided credentials.
  6. Verify correct use of dedicated service accounts and plan node-role permissions reduction

    • On any machine with kubectl access, confirm the running pod uses the intended service account:
      kubectl -n my-namespace get pod -l app=my-app -o jsonpath='{.items[0].spec.serviceAccountName}'; echo
    • Inside the pod, validate AWS identity (replace pod name and container as appropriate):
      kubectl -n my-namespace exec -it my-pod -- aws sts get-caller-identity
      The returned ARN should be the IRSA role (for example arn:aws:iam::ACCOUNT_ID:role/my-workload-role), not the node instance profile.
    • Once a sufficient number of workloads have migrated to IRSA, review and tighten the IAM policy attached to the EKS node instance role in the AWS console or via:
      aws iam list-attached-role-policies --role-name my-node-instance-role
      Detach or narrow any application-specific permissions so that pods are no longer relying on the node role for AWS API access.
Using kubectl

kubectl can’t change how pods obtain AWS credentials or how IAM roles are bound to service accounts; this is configured in the EKS control plane and AWS IAM (console/CLI/IaC). To address this finding, follow the guidance in the Manual Steps section using AWS tooling rather than kubectl.

Automation
#!/usr/bin/env bash
# Report pods that may be using node IAM roles instead of dedicated IRSA roles
# Run on: any machine with kubectl access and AWS CLI configured
# Requirements: kubectl, jq, aws

set -euo pipefail

# --- configuration (edit as needed) ---
# Namespace(s) to include; leave empty for all namespaces
NAMESPACES="" # e.g. "default kube-system app-namespace"
# Known service accounts that SHOULD have IRSA roles (optional allowlist)
KNOWN_IRSA_SA="" # e.g. "kube-system:external-dns prod:app-sa"

echo "== Detect pods likely using node IAM roles (no IRSA) ==" >&2

# Helper: list namespaces
if [[ -z "${NAMESPACES}" ]]; then
mapfile -t NS_LIST < <(kubectl get ns -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}')
else
read -ra NS_LIST <<< "${NAMESPACES}"
fi

tmpdir="$(mktemp -d)"
trap 'rm -rf "${tmpdir}"' EXIT

# 1. Identify service accounts that are configured for IRSA (have eks.amazonaws.com/role-arn)
echo "== ServiceAccounts with IRSA annotation (eks.amazonaws.com/role-arn) =="

for ns in "${NS_LIST[@]}"; do
kubectl get sa -n "${ns}" -o json \
| jq -r --arg ns "${ns}" '
.items[]
| select(.metadata.annotations["eks.amazonaws.com/role-arn"] != null)
| "\($ns):\(.metadata.name) -> \(.metadata.annotations["eks.amazonaws.com/role-arn"])"
' || true
done | tee "${tmpdir}/irsa-sas.txt"

echo
echo "== Pods and their service accounts (cluster-wide) =="

# 2. List all pods and their SAs, marking whether the SA has IRSA
{
printf "NAMESPACE\tPOD\tSERVICE_ACCOUNT\tHAS_IRSA_SA_ANNOTATION\n"
for ns in "${NS_LIST[@]}"; do
kubectl get pods -n "${ns}" -o json \
| jq -r --arg ns "${ns}" '
.items[]
| [
.metadata.namespace,
.metadata.name,
(.spec.serviceAccountName // "default"),
(if .spec.serviceAccountName == null then "UNKNOWN"
else "CHECK"
end)
]
| @tsv
' || true
done
} | while IFS=$'\t' read -r ns pod sa flag; do
if [[ "${ns}" == "NAMESPACE" ]]; then
echo -e "${ns}\t${pod}\t${sa}\t${flag}"
continue
fi
# Determine if SA has IRSA annotation
if kubectl get sa "${sa}" -n "${ns}" -o json 2>/dev/null \
| jq -e '.metadata.annotations["eks.amazonaws.com/role-arn"]' >/dev/null; then
has_irsa="yes"
else
has_irsa="no"
fi
echo -e "${ns}\t${pod}\t${sa}\t${has_irsa}"
done | tee "${tmpdir}/pods-sa-irsa.txt"

echo
echo "== Summary: pods whose service accounts DO NOT have an IRSA role (potentially using node IAM role) =="

awk 'NR==1 {print; next} $4=="no" {print}' "${tmpdir}/pods-sa-irsa.txt" \
| tee "${tmpdir}/pods-missing-irsa.txt"

echo
echo "== Optional: Check if node IAM role has broad permissions (coarse indicator) =="

# 3. Identify node IAM role(s) used by the EKS worker nodes (if using EKS-managed or self-managed nodes with IAM roles)
# This section is informational; you may need to adapt for your environment.

# Try to derive the cluster name from current context
CURRENT_CONTEXT="$(kubectl config current-context 2>/dev/null || true)"
CLUSTER_NAME=""
if [[ -n "${CURRENT_CONTEXT}" ]]; then
# Common eks context pattern: arn:aws:eks:region:account:cluster/CLUSTER_NAME
CLUSTER_NAME="$(aws eks list-clusters --output json 2>/dev/null \
| jq -r --arg ctx "${CURRENT_CONTEXT##*/}" '
.clusters[]
| select(. == $ctx)
' || true)"
fi

if [[ -z "${CLUSTER_NAME}" ]]; then
echo "Could not reliably determine EKS cluster name from kubectl context; skipping node IAM role analysis." >&2
else
echo "Using EKS cluster: ${CLUSTER_NAME}" >&2

# Describe nodegroups and pull node IAM roles (for managed nodegroups)
echo "Managed nodegroup IAM roles (if any):" >&2
aws eks list-nodegroups --cluster-name "${CLUSTER_NAME}" --output json \
| jq -r '.nodegroups[]' \
| while read -r ng; do
aws eks describe-nodegroup --cluster-name "${CLUSTER_NAME}" --nodegroup-name "${ng}" --output json \
| jq -r --arg ng "${ng}" '
.nodegroup.nodeRole
| "nodegroup \($ng) IAM role: \(. )"
'
done 2>/dev/null || true
fi

cat <<'EOF'

INTERPRETING THE OUTPUT:

1) The "ServiceAccounts with IRSA annotation" section lists all Kubernetes service
accounts that already have an IAM role associated via the
eks.amazonaws.com/role-arn annotation. Pods using these SAs are using IRSA.

2) The "Pods and their service accounts (cluster-wide)" and "Summary" sections:
- Any pod with HAS_IRSA_SA_ANNOTATION = "no" is a candidate for review.
- These pods are likely falling back to the node IAM role to access AWS APIs.

3) A finding (POTENTIAL PROBLEM) is indicated when:
- A workload requires AWS API access AND
- Its service account does not have eks.amazonaws.com/role-arn set (HAS_IRSA_SA_ANNOTATION = "no"), AND
- The node IAM role attached to the worker nodes has broad permissions that are
not least-privilege relative to that workload.

NEXT STEPS (MANUAL REVIEW, NOT AUTOMATED FIX):

- For each pod listed in "pods-missing-irsa.txt":
* Determine whether the application inside actually calls AWS APIs
(review code, images, environment variables, configuration).
* If it does, plan to:
- Create a dedicated Kubernetes service account for that workload.
- Create an IAM role with least-privilege permissions.
- Annotate the service account with eks.amazonaws.com/role-arn.
- Update the workload spec to use that service account.
* If it does not use AWS at all, no change is required for this control.

EOF

Additional Reading: