> ## 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.

# Minimize Cluster Access Read Only For Amazon Ecr

### More Info:

Scan images being deployed to Amazon EKS for vulnerabilities.

### Risk Level

Medium

### 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)
* NIS2 Directive
* 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

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

    <AccordionGroup>
      <Accordion title="Manual Steps" defaultOpen="true">
        1. **Identify node IAM roles used by the cluster**
           * On any machine with AWS CLI access and the right permissions, list the nodegroups:
             ```bash theme={null}
             aws eks list-nodegroups --cluster-name YOUR_CLUSTER_NAME
             ```
           * For each nodegroup, get its IAM role:
             ```bash theme={null}
             aws eks describe-nodegroup \
               --cluster-name YOUR_CLUSTER_NAME \
               --nodegroup-name YOUR_NODEGROUP_NAME \
               --query 'nodegroup.nodeRole' \
               --output text
             ```

        2. **Review current ECR-related permissions on each node IAM role**
           * For each `NODE_ROLE_ARN` from step 1, list inline and attached policies:
             ```bash theme={null}
             ROLE_NAME=$(basename NODE_ROLE_ARN)

             aws iam list-attached-role-policies --role-name "$ROLE_NAME"
             aws iam list-role-policies --role-name "$ROLE_NAME"
             ```
           * For each attached or inline policy, view the document:
             ```bash theme={null}
             aws iam get-role-policy \
               --role-name "$ROLE_NAME" \
               --policy-name INLINE_POLICY_NAME

             aws iam get-policy \
               --policy-arn POLICY_ARN

             aws iam get-policy-version \
               --policy-arn POLICY_ARN \
               --version-id VERSION_ID_FROM_PREVIOUS_COMMAND
             ```

        3. **Assess whether permissions are restricted to read-only ECR actions**
           * In each policy document, locate `Action` entries containing `ecr:` and verify they are limited to:
             * `ecr:BatchCheckLayerAvailability`
             * `ecr:BatchGetImage`
             * `ecr:GetDownloadUrlForLayer`
             * `ecr:GetAuthorizationToken`
           * Record any additional `ecr:*` actions (e.g., `ecr:PutImage`, `ecr:BatchDeleteImage`, `ecr:*`) that go beyond these read-only requirements.

        4. **Decide and plan corrections (principle of least privilege)**
           * For each role where extra ECR actions are present, decide whether they are truly needed (for example, by CI/CD or admin tooling) or can be removed.
           * If non-read-only ECR actions are needed, consider moving those permissions to a separate IAM principal (e.g., a CI user/role) rather than the node role.

        5. **Correct IAM policies to enforce read-only ECR access**
           * To attach a dedicated read-only ECR policy to the node role (if missing), create it once:
             ```bash theme={null}
             aws iam create-policy \
               --policy-name EKSNodeECRReadOnly \
               --policy-document '{
                 "Version": "2012-10-17",
                 "Statement": [
                   {
                     "Effect": "Allow",
                     "Action": [
                       "ecr:BatchCheckLayerAvailability",
                       "ecr:BatchGetImage",
                       "ecr:GetDownloadUrlForLayer",
                       "ecr:GetAuthorizationToken"
                     ],
                     "Resource": "*"
                   }
                 ]
               }'
             ```
           * Attach it to each node role:
             ```bash theme={null}
             aws iam attach-role-policy \
               --role-name "$ROLE_NAME" \
               --policy-arn arn:aws:iam::ACCOUNT_ID:policy/EKSNodeECRReadOnly
             ```
           * Edit existing inline or customer-managed policies to remove any ECR actions other than the four listed above. For customer-managed policies, update the policy document and create a new version:
             ```bash theme={null}
             aws iam create-policy-version \
               --policy-arn POLICY_ARN \
               --policy-document file://updated-policy.json \
               --set-as-default
             ```

        6. **Verify resulting permissions**
           * Re-fetch each node role’s effective ECR permissions and confirm that the only remaining `ecr:` actions are:\
             `ecr:BatchCheckLayerAvailability`, `ecr:BatchGetImage`, `ecr:GetDownloadUrlForLayer`, and `ecr:GetAuthorizationToken`:
             ```bash theme={null}
             aws iam list-attached-role-policies --role-name "$ROLE_NAME"
             aws iam list-role-policies --role-name "$ROLE_NAME"

             # For each policy, re-run the get-policy/get-policy-version/get-role-policy
             # commands from step 2 and inspect all "Action" fields containing "ecr:"
             ```
      </Accordion>

      <Accordion title="Using kubectl">
        kubectl can’t modify IAM permissions or Amazon ECR access; this configuration is managed in AWS IAM and (optionally) your IaC, not via Kubernetes API objects. To adjust node IAM roles and ECR permissions, follow the guidance in the Manual Steps section using the AWS console/CLI or your infrastructure-as-code tooling.
      </Accordion>

      <Accordion title="Automation">
        ```bash theme={null}
        #!/usr/bin/env bash
        # Report ECR access level for EKS node IAM roles across the cluster
        # Requirements: aws CLI configured, kubectl configured, jq

        set -euo pipefail

        echo "=== Discovering node IAM roles used by this EKS cluster ==="
        # Run on any machine with kubectl and aws configured

        # Get the AWS account and region from current identity/config
        ACCOUNT_ID="$(aws sts get-caller-identity --query 'Account' --output text)"
        REGION="$(aws configure get region || echo '')"
        if [ -z "$REGION" ]; then
          echo "ERROR: AWS region not set in aws CLI config"
          exit 1
        fi

        # 1. List distinct IAM roles from node instance profiles (IRSA roles are separate and not ECR pullers by default)
        #    We infer roles from EC2 instances that are EKS worker nodes (tagged with kubernetes.io/cluster/<cluster-name>)
        #    First, get cluster name from kubeconfig/current-context
        CLUSTER_NAME="$(kubectl config view --minify -o jsonpath='{.clusters[0].name}' | sed 's/^arn:aws:eks:[^:]*:[0-9]\+:cluster\///')"

        if [ -z "$CLUSTER_NAME" ]; then
          echo "ERROR: Could not derive EKS cluster name from kubeconfig"
          exit 1
        fi

        echo "Detected cluster name: ${CLUSTER_NAME}"
        echo "Using AWS account: ${ACCOUNT_ID}, region: ${REGION}"
        echo

        # Find EC2 instances that belong to this EKS cluster
        INSTANCE_IDS=$(aws ec2 describe-instances \
          --region "$REGION" \
          --filters "Name=tag:kubernetes.io/cluster/${CLUSTER_NAME},Values=owned,shared" \
          --query 'Reservations[].Instances[].InstanceId' \
          --output text || true)

        if [ -z "$INSTANCE_IDS" ]; then
          echo "No EC2 worker instances found for cluster tag kubernetes.io/cluster/${CLUSTER_NAME}"
          echo "If using Fargate-only cluster, node IAM role analysis is not applicable."
          exit 0
        fi

        # Map instances -> instance profiles -> IAM roles
        ROLE_ARNS=$(aws ec2 describe-instances \
          --region "$REGION" \
          --instance-ids $INSTANCE_IDS \
          --query 'Reservations[].Instances[].IamInstanceProfile.Arn' \
          --output text | tr '\t' '\n' | sort -u | while read -r PROFILE_ARN; do
            [ -z "$PROFILE_ARN" ] && continue
            PROFILE_NAME="$(basename "$PROFILE_ARN")"
            aws iam get-instance-profile --instance-profile-name "$PROFILE_NAME" \
              --query 'InstanceProfile.Roles[].Arn' --output text
          done | tr '\t' '\n' | sort -u)

        if [ -z "$ROLE_ARNS" ]; then
          echo "No IAM roles associated with worker instances were found."
          exit 0
        fi

        echo "=== Node IAM roles detected ==="
        echo "$ROLE_ARNS"
        echo

        echo "=== Checking ECR permissions for each node IAM role ==="
        echo

        for ROLE_ARN in $ROLE_ARNS; do
          ROLE_NAME="$(basename "$ROLE_ARN")"
          echo "----- Role: ${ROLE_NAME} (${ROLE_ARN}) -----"

          # Get all inline and attached policies as a single JSON policy set
          TMP_POLICIES=$(mktemp)

          # Inline policies
          aws iam list-role-policies --role-name "$ROLE_NAME" --query 'PolicyNames' --output text | tr '\t' '\n' | while read -r PNAME; do
            [ -z "$PNAME" ] && continue
            aws iam get-role-policy --role-name "$ROLE_NAME" --policy-name "$PNAME" \
              --query 'PolicyDocument' --output json
          done | jq -s '.' > "$TMP_POLICIES"

          # Attached managed policies
          ATTACHED_ARNS=$(aws iam list-attached-role-policies --role-name "$ROLE_NAME" \
            --query 'AttachedPolicies[].PolicyArn' --output text || true)
          for PARN in $ATTACHED_ARNS; do
            VERSION_ID=$(aws iam get-policy --policy-arn "$PARN" \
              --query 'Policy.DefaultVersionId' --output text)
            aws iam get-policy-version --policy-arn "$PARN" --version-id "$VERSION_ID" \
              --query 'PolicyVersion.Document' --output json
          done | jq -s '.[. != null][]' >> "$TMP_POLICIES" || true

          if ! [ -s "$TMP_POLICIES" ]; then
            echo "NO POLICIES FOUND on this role (cannot pull from ECR)."
            echo
            rm -f "$TMP_POLICIES"
            continue
          fi

          # Normalize to an array of statements
          EFFECTIVE=$(jq -s '[.[][] | .Statement] | flatten' "$TMP_POLICIES")

          rm -f "$TMP_POLICIES"

          echo "ECR-related statements (any non-read or non-ECR access is a potential problem):"
          echo "$EFFECTIVE" | jq '
            map(select(
              (.Action|tostring|test("(^|:)ecr:")) or
              ((.Action|arrays?//[.]) | any(test("(^|:)ecr:")))
            ))' || echo "  (none)"

          echo
          echo "Summary for ${ROLE_NAME}:"
          # Check for required minimal read-only ECR actions with Resource:* (per benchmark)
          for ACTION in ecr:BatchCheckLayerAvailability ecr:BatchGetImage ecr:GetDownloadUrlForLayer ecr:GetAuthorizationToken; do
            HAS_ACTION=$(echo "$EFFECTIVE" | jq --arg A "$ACTION" '
              any(.[]; .Effect=="Allow" and
                ((.Action===$A) or
                 (.Action|type=="array" and any(.==$A)) or
                 (.Action|type=="string" and .=="ecr:*" or .=="*") or
                 (.Action|type=="array" and any(.=="ecr:*" or .=="*"))
                )
              )')
            printf "  %-30s: %s\n" "$ACTION" "$HAS_ACTION"
          done

          # Flag overly broad actions
          BROAD=$(echo "$EFFECTIVE" | jq '
            map(select(.Effect=="Allow" and
              (
                .Action=="ecr:*" or .Action=="*" or
                (.Action|type=="array" and any(.=="ecr:*" or .=="*"))
              )
            ))')
          if [ "$(echo "$BROAD" | jq 'length')" -gt 0 ]; then
            echo "  WARNING: Broad ECR or wildcard permissions detected:"
            echo "$BROAD" | jq '.'
          else
            echo "  No wildcard ecr:* or * permissions found."
          fi

          # Flag non-read ECR permissions (push/delete/admin)
          NON_READ=$(echo "$EFFECTIVE" | jq '
            map(select(.Effect=="Allow") |
                .Action as $a |
                if ($a|type)=="string" then [$a] else $a end |
                map(select(
                  test("(^|:)ecr:Put") or
                  test("(^|:)ecr:Delete") or
                  test("(^|:)ecr:Upload") or
                  test("(^|:)ecr:CompleteLayerUpload") or
                  test("(^|:)ecr:InitiateLayerUpload") or
                  test("(^|:)ecr:BatchDeleteImage") or
                  test("(^|:)ecr:SetRepositoryPolicy") or
                  test("(^|:)ecr:TagResource") or
                  test("(^|:)ecr:UntagResource") or
                  test("(^|:)ecr:ReplicateImage") or
                  test("(^|:)ecr:Create") or
                  test("(^|:)ecr:Delete") or
                  test("(^|:)ecr:Update")
                )) | select(length>0) | .) | map(select(length>0))')
          if [ "$(echo "$NON_READ" | jq 'length')" -gt 0 ]; then
            echo "  WARNING: Non-read ECR permissions detected (potential write/admin capabilities):"
            echo "$NON_READ" | jq '.'
          else
            echo "  No non-read ECR permissions detected."
          fi

          echo
        done

        echo "=== How to interpret this report ==="
        cat <<'EOF'
        Problematic output indicates:

        - Missing required read-only ECR actions:
          Any "false" next to:
            ecr:BatchCheckLayerAvailability
            ecr:BatchGetImage
            ecr:GetDownloadUrlForLayer
            ecr:GetAuthorizationToken
          means nodes may not be able to pull from ECR correctly.

        - Overly broad or write/admin access:
          - "WARNING: Broad ECR or wildcard permissions detected" with statements using "ecr:*" or "*":
            This suggests more than read-only access, violating the principle of least privilege.
          - "WARNING: Non-read ECR permissions detected":
            Any listed actions (Put*, Delete*, Upload*, BatchDeleteImage, SetRepositoryPolicy, Tag/Untag, Create*/Update*) mean
            the node role can modify repositories or images, which should generally be restricted.

        Use this report to manually adjust IAM policies so node roles have only the four required ECR read-only actions
        and avoid wildcard or write/admin ECR permissions.
        EOF
        ```
      </Accordion>
    </AccordionGroup>
  </Tab>
</Tabs>

### Additional Reading:

* [https://docs.aws.amazon.com/AmazonECR/latest/userguide/image-scanning.html](https://docs.aws.amazon.com/AmazonECR/latest/userguide/image-scanning.html)
