> ## Documentation Index
> Fetch the complete documentation index at: https://cloudanix.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# 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

<Tabs>
  <Tab title="Remediation">
    ### Remediation

    <AccordionGroup>
      <Accordion title="Manual Steps" defaultOpen="true">
        1. **Identify pods that use AWS APIs and how they get credentials**
           * On any machine with AWS CLI access:
             ```bash theme={null}
             aws eks list-clusters
             aws eks describe-cluster --name <CLUSTER_NAME> --query "cluster.identity.oidc"
             ```
           * On any machine with `kubectl` access:
             ```bash theme={null}
             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`).

        2. **Confirm OIDC provider and IRSA capability for the cluster**
           * On any machine with AWS CLI access:
             ```bash theme={null}
             aws eks describe-cluster --name <CLUSTER_NAME> --query "cluster.identity.oidc.issuer" --output text
             ```
           * If this returns `None` or 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.

        3. **Review IAM policies attached to worker node roles for overbroad AWS access**
           * On any machine with AWS CLI access:
             ```bash theme={null}
             # List nodegroup IAM roles
             aws 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 role
             aws 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).

        4. **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`:
             ```json theme={null}
             {
               "Version": "2012-10-17",
               "Statement": [
                 {
                   "Effect": "Allow",
                   "Action": ["s3:GetObject"],
                   "Resource": ["arn:aws:s3:::my-bucket/path/*"]
                 }
               ]
             }
             ```
           * On any machine with AWS CLI access:
             ```bash theme={null}
             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:AssumeRoleWithWebIdentity` to the specific `system:serviceaccount:<NAMESPACE>:<SERVICEACCOUNT_NAME>`.
             * Attach the app’s IAM policy created above.

        5. **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:
               ```yaml theme={null}
               apiVersion: v1
               kind: ServiceAccount
               metadata:
                 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.
           * After deployment, confirm the annotation is present:
             ```bash theme={null}
             kubectl get sa <SERVICEACCOUNT_NAME> -n <NAMESPACE> -o yaml | grep -A2 "eks.amazonaws.com/role-arn"
             ```

        6. **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:
               ```bash theme={null}
               aws iam detach-role-policy \
                 --role-name <NODE_ROLE_NAME> \
                 --policy-arn arn:aws:iam::<ACCOUNT_ID>:policy/<POLICY_TO_REMOVE>
               ```
           * Validate credential isolation and least privilege:
             * From a pod that should have IRSA, check the AWS identity:
               ```bash theme={null}
               kubectl exec -n <NAMESPACE> <POD_NAME> -- \
                 aws sts get-caller-identity
               ```
               Confirm the ARN matches the dedicated IRSA role.
             * From a pod that should not have AWS access, run the same command and confirm it fails or uses a different, appropriate role.
      </Accordion>

      <Accordion title="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.
      </Accordion>

      <Accordion title="Automation">
        ```bash theme={null}
        #!/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:

        ```bash theme={null}
        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
        ```
      </Accordion>
    </AccordionGroup>
  </Tab>
</Tabs>
