> ## 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 To Read-Only For Amazon ECR

### More Info:

Grant the EKS worker node IAM role only read-only ECR permissions so nodes can pull images but cannot modify the registry.

### 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 the worker node IAM role (NodeInstanceRole)**
           * From any machine with AWS CLI configured for the EKS account:
             ```bash theme={null}
             aws eks describe-nodegroup \
               --cluster-name <CLUSTER_NAME> \
               --nodegroup-name <NODEGROUP_NAME> \
               --query 'nodegroup.nodeRole' \
               --output text
             ```
           * Record the IAM role ARN returned (this is the worker node role).

        2. **List IAM policies currently attached to the worker node role**
           * From any machine with AWS CLI:
             ```bash theme={null}
             ROLE_NAME=$(basename <ROLE_ARN_FROM_STEP_1>)
             aws iam list-attached-role-policies \
               --role-name "$ROLE_NAME"
             aws iam list-role-policies \
               --role-name "$ROLE_NAME"
             ```
           * Note any customer-managed or inline policies that mention `ecr:` actions.

        3. **Review current ECR permissions for the worker node role**
           * For each attached managed policy ARN from step 2:
             ```bash theme={null}
             aws iam get-policy --policy-arn <POLICY_ARN>
             aws iam get-policy-version \
               --policy-arn <POLICY_ARN> \
               --version-id <DEFAULT_VERSION_ID_FROM_PREVIOUS_COMMAND>
             ```
           * For each inline policy name from step 2:
             ```bash theme={null}
             aws iam get-role-policy \
               --role-name "$ROLE_NAME" \
               --policy-name <INLINE_POLICY_NAME>
             ```
           * Inspect for any ECR actions beyond:
             * `ecr:BatchCheckLayerAvailability`
             * `ecr:BatchGetImage`
             * `ecr:GetDownloadUrlForLayer`
             * `ecr:GetAuthorizationToken`

        4. **Decide and adjust: restrict ECR access to read-only if appropriate**
           * If the role has broader ECR permissions (for example `ecr:*`, `ecr:PutImage`, `ecr:DeleteRepository`, `ecr:BatchDeleteImage`):
             * Confirm no operational dependency requires these write/administrative ECR actions from worker nodes.
             * Create or update a **dedicated** IAM policy for worker nodes that contains only the benchmark-recommended ECR statement:
               ```bash theme={null}
               cat > eks-node-ecr-readonly.json << 'EOF'
               {
                 "Version": "2012-10-17",
                 "Statement": [
                   {
                     "Effect": "Allow",
                     "Action": [
                       "ecr:BatchCheckLayerAvailability",
                       "ecr:BatchGetImage",
                       "ecr:GetDownloadUrlForLayer",
                       "ecr:GetAuthorizationToken"
                     ],
                     "Resource": "*"
                   }
                 ]
               }
               EOF

               aws iam create-policy \
                 --policy-name eks-node-ecr-readonly \
                 --policy-document file://eks-node-ecr-readonly.json
               ```
             * Attach this policy to the worker node role:
               ```bash theme={null}
               aws iam attach-role-policy \
                 --role-name "$ROLE_NAME" \
                 --policy-arn arn:aws:iam::<ACCOUNT_ID>:policy/eks-node-ecr-readonly
               ```
             * Detach or edit any existing policies that grant broader ECR access, ensuring other non-ECR permissions still required by nodes are preserved.

        5. **(Optional) Use separate role for ECR admin/write tasks**
           * If cluster components or automation need to push/manage images, create a separate IAM role or user with the required `ecr:*` write/admin permissions, and configure CI/CD or admin tools to use that identity rather than the worker node role.

        6. **Verify resulting permissions match the benchmark intent**
           * From any machine with AWS CLI:
             ```bash theme={null}
             aws iam list-attached-role-policies --role-name "$ROLE_NAME"
             aws iam list-role-policies --role-name "$ROLE_NAME"
             ```
           * Re-fetch each policy as in step 3 and confirm:
             * The worker node role has only the four ECR actions listed in the remediation (and no additional ECR write/admin actions), and
             * Other non-ECR permissions required for node operation (e.g., CloudWatch logs, SSM, autoscaling) remain as approved.
      </Accordion>

      <Accordion title="Using kubectl">
        kubectl cannot modify IAM roles or Amazon ECR permissions; this finding is fixed in AWS IAM and/or your IaC, not via Kubernetes API objects. See the Manual Steps section for how to update the EKS worker node IAM role (NodeInstanceRole) with the required read‑only ECR policy.
      </Accordion>

      <Accordion title="Automation">
        ```bash theme={null}
        #!/usr/bin/env bash
        # Report ECR-related IAM permissions used by an EKS cluster's nodes.
        # Run on: any machine with awscli, jq, and kubectl configured for the target cluster/account.

        set -euo pipefail

        REQUIRED_ACTIONS=(
          "ecr:BatchCheckLayerAvailability"
          "ecr:BatchGetImage"
          "ecr:GetDownloadUrlForLayer"
          "ecr:GetAuthorizationToken"
        )

        echo "== Discovering node IAM roles from Kubernetes nodes =="

        # Try IRSA-style IAM roles for service accounts used by nodes (e.g., managed nodegroups)
        echo
        echo "Kubernetes nodes and their IAM roles (if discoverable):"
        kubectl get nodes -o json | jq -r '
          .items[]
          | {
              name: .metadata.name,
              sa: .spec.providerID,
              labels: .metadata.labels
            }
        ' 2>/dev/null || echo "Warning: could not parse node IAM roles from providerID; will fall back to EC2 instance roles."

        # Fallback: use AWS APIs to list Auto Scaling Groups / Launch Templates / NodeInstanceRoles

        echo
        echo "== Discovering node instance IAM roles via AWS APIs =="

        ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
        REGION=$(aws configure get region)
        echo "Using AWS account: $ACCOUNT_ID  region: $REGION"
        echo

        # List EKS clusters in this account/region
        echo "EKS clusters in this region:"
        aws eks list-clusters --region "$REGION" --output text || true
        echo

        # Get all EC2 instances that are part of this cluster (using the kubernetes.io/cluster/* tag)
        echo "EC2 instances that appear to be EKS worker nodes:"
        aws ec2 describe-instances \
          --filters "Name=tag-key,Values=kubernetes.io/cluster/*" "Name=instance-state-name,Values=running" \
          --query 'Reservations[].Instances[].[InstanceId, IamInstanceProfile.Arn, Tags]' \
          --output json | jq -r '
            .[]? | @tsv
          ' || true
        echo

        # Collect all distinct instance profile ARNs from running EKS worker nodes
        PROFILE_ARNS=$(aws ec2 describe-instances \
          --filters "Name=tag-key,Values=kubernetes.io/cluster/*" "Name=instance-state-name,Values=running" \
          --query 'Reservations[].Instances[].IamInstanceProfile.Arn' \
          --output text | tr '\t' '\n' | sort -u)

        if [ -z "$PROFILE_ARNS" ]; then
          echo "No IAM instance profiles found for running EKS worker nodes in this region."
          exit 0
        fi

        echo "== Evaluating IAM policies attached to worker node instance roles =="
        echo

        for PROFILE_ARN in $PROFILE_ARNS; do
          PROFILE_NAME=$(basename "$PROFILE_ARN")

          ROLE_ARN=$(aws iam get-instance-profile \
            --instance-profile-name "$PROFILE_NAME" \
            --query 'InstanceProfile.Roles[0].Arn' \
            --output text 2>/dev/null || echo "")

          if [ -z "$ROLE_ARN" ] || [ "$ROLE_ARN" = "None" ]; then
            echo "Instance profile $PROFILE_NAME has no attached role; skipping."
            continue
          fi

          ROLE_NAME=$(basename "$ROLE_ARN")
          echo "----"
          echo "Instance profile: $PROFILE_NAME"
          echo "Node IAM role:    $ROLE_NAME"

          # List all policies (inline + attached) for the role
          echo "Attached managed policies:"
          aws iam list-attached-role-policies \
            --role-name "$ROLE_NAME" \
            --query 'AttachedPolicies[].PolicyArn' \
            --output text || true

          echo
          echo "Inline policies:"
          aws iam list-role-policies \
            --role-name "$ROLE_NAME" \
            --output text || true
          echo

          # Build a list of policy documents (inline + managed) and check for ECR actions
          echo "ECR-related permissions for role $ROLE_NAME:"
          TMP_FILE=$(mktemp)

          # Inline policies
          INLINE_POLICY_NAMES=$(aws iam list-role-policies --role-name "$ROLE_NAME" --query 'PolicyNames' --output text)
          for PNAME in $INLINE_POLICY_NAMES; do
            aws iam get-role-policy \
              --role-name "$ROLE_NAME" \
              --policy-name "$PNAME" \
              --query 'PolicyDocument' \
              --output json >> "$TMP_FILE"
          done

          # Managed policies
          MANAGED_POLICY_ARNS=$(aws iam list-attached-role-policies \
            --role-name "$ROLE_NAME" \
            --query 'AttachedPolicies[].PolicyArn' \
            --output text)

          for PARN in $MANAGED_POLICY_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 >> "$TMP_FILE"
          done

          if [ ! -s "$TMP_FILE" ]; then
            echo "  No policy documents found for role $ROLE_NAME."
            rm -f "$TMP_FILE"
            continue
          fi

          echo "  All ECR-related actions found:"
          jq -r '
            (.Statement // []) as $s
            | if (type=="object" and .Statement) then [.Statement] else $s end
          ' "$TMP_FILE" 2>/dev/null | jq -r '
            .[]? 
            | select(.Action != null)
            | .Effect as $effect
            | (if (.Action|type)=="string" then [.Action] else .Action end) as $acts
            | $acts[]
            | select(test("^ecr:"; "i"))
            | "\($effect)\t\(.)"
          ' | sort -u | awk '{print "   - "$0}' || echo "   - none"

          echo
          echo "  REQUIRED minimal ECR actions (per benchmark):"
          for a in "${REQUIRED_ACTIONS[@]}"; do
            echo "   - $a"
          done

          echo
          echo "  EXTRA/WIDER ECR permissions (POTENTIAL PROBLEM):"
          # Anything ecr:* that is NOT in the required list is suspicious
          jq -r '
            (.Statement // []) as $s
            | if (type=="object" and .Statement) then [.Statement] else $s end
          ' "$TMP_FILE" 2>/dev/null | jq -r '
            .[]? 
            | select(.Action != null)
            | .Effect as $effect
            | (if (.Action|type)=="string" then [.Action] else .Action end) as $acts
            | $acts[]
            | select(test("^ecr:"; "i"))
            | "\($effect)\t\(.)"
          ' | sort -u | while read -r EFFECT ACTION; do
              IS_REQUIRED=false
              for req in "${REQUIRED_ACTIONS[@]}"; do
                if [ "$ACTION" = "$req" ]; then
                  IS_REQUIRED=true
                  break
                fi
              done
              if [ "$IS_REQUIRED" = false ]; then
                echo "   - $EFFECT $ACTION"
              fi
            done

          rm -f "$TMP_FILE"
          echo
        done

        echo "== Interpretation =="
        cat <<'EOF'
        Any node IAM role that:
          - is missing one of the required actions:
              ecr:BatchCheckLayerAvailability
              ecr:BatchGetImage
              ecr:GetDownloadUrlForLayer
              ecr:GetAuthorizationToken
            may fail to pull images from ECR, and requires remediation to ADD those read-only actions.

          - shows EXTRA/WIDER ECR permissions (for example ecr:*, ecr:PutImage, ecr:DeleteRepository, etc.)
            under the "EXTRA/WIDER ECR permissions (POTENTIAL PROBLEM)" section is OVER-PRIVILEGED and
            should be manually reviewed. The goal is to restrict the node role to ONLY the four
            read-only actions above, unless there is a strong, documented business need.
        EOF
        ```

        **What output indicates a problem**

        * Under `ECR-related permissions for role ...`:
          * If any of the four required actions are missing, nodes may not be able to pull images.
        * Under `EXTRA/WIDER ECR permissions (POTENTIAL PROBLEM)`:
          * Any listed actions (e.g., `Allow ecr:*`, `Allow ecr:PutImage`, `Allow ecr:DeleteRepository*`) indicate the node role is not read-only and needs manual review and likely tightening in IAM.
      </Accordion>
    </AccordionGroup>
  </Tab>
</Tabs>
