Skip to main content

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​

Remediation​

Manual Steps
  1. Identify the worker node IAM role (NodeInstanceRole)

    • From any machine with AWS CLI configured for the EKS account:
      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:
      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:
      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:
      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:
        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:
        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:
      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.
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.

Automation
#!/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.