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
Remediation​
Manual Steps
-
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).
- From any machine with AWS CLI configured for the EKS account:
-
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.
- From any machine with AWS CLI:
-
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:BatchCheckLayerAvailabilityecr:BatchGetImageecr:GetDownloadUrlForLayerecr:GetAuthorizationToken
- For each attached managed policy ARN from step 2:
-
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": "*"}]}EOFaws 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.
- If the role has broader ECR permissions (for example
-
(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.
- If cluster components or automation need to push/manage images, create a separate IAM role or user with the required
-
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.
- From any machine with AWS CLI:
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.
- Any listed actions (e.g.,