Minimize User Access To Amazon ECR
More Info:​
Restrict IAM user and role access to Amazon ECR to only what is required, using least-privilege IAM policies.
Risk Level​
Medium
Address​
Security
Compliance Standards​
- CIS EKS
Triage and Remediation​
- Remediation
Remediation​
Manual Steps
-
Inventory who can access ECR
- On any machine with AWS CLI configured for the account:
aws ecr describe-repositories --region us-east-1aws iam list-usersaws iam list-roles
- In the AWS Console: IAM → Access Analyzer → Analyze policies (optional) to see which principals have ECR access.
- On any machine with AWS CLI configured for the account:
-
Identify IAM policies that grant ECR permissions
- List all policies that mention
ecr:actions:aws iam list-policies --scope Local --only-attached \--query 'Policies[].Arn' --output text | tr '\t' '\n' | while read arn; doaws iam get-policy-version \--policy-arn "$arn" \--version-id "$(aws iam get-policy --policy-arn "$arn" --query 'Policy.DefaultVersionId' --output text)" \| jq -e '.PolicyVersion.Document.Statement[]?.Action? | tostring | test("ecr:")' >/dev/null && echo "$arn"done - For each returned policy ARN, inspect details:
aws iam get-policy --policy-arn <POLICY_ARN>aws iam get-policy-version \--policy-arn <POLICY_ARN> \--version-id <VERSION_ID>
- List all policies that mention
-
Review scope for over-privilege
- For each policy, check for:
- Wildcard actions (e.g.,
"Action": "ecr:*"). - Wildcard resources (e.g.,
"Resource": "*", or all repositories). - Principals that do not need ECR access (users/roles not tied to CI/CD or workloads that pull/push images).
- Wildcard actions (e.g.,
- Use console: IAM → Policies → (select policy) → JSON tab; verify actions and resources are narrowed to required repositories and operations.
- For each policy, check for:
-
Redesign least-privilege policies based on actual need
- Determine for each user/role what they must do:
- Pull only (e.g., node roles, application pods):
ecr:GetAuthorizationToken,ecr:BatchCheckLayerAvailability,ecr:GetDownloadUrlForLayer,ecr:BatchGetImage. - Push & pull (CI/CD): above plus
ecr:PutImage,ecr:InitiateLayerUpload,ecr:UploadLayerPart,ecr:CompleteLayerUpload,ecr:ListImages.
- Pull only (e.g., node roles, application pods):
- Create a new scoped policy (example for one repository) with the console or CLI:
cat > ecr-push-pull-single-repo.json << 'EOF'{"Version": "2012-10-17","Statement": [{"Effect": "Allow","Action": ["ecr:GetAuthorizationToken"],"Resource": "*"},{"Effect": "Allow","Action": ["ecr:BatchCheckLayerAvailability","ecr:GetDownloadUrlForLayer","ecr:BatchGetImage","ecr:PutImage","ecr:InitiateLayerUpload","ecr:UploadLayerPart","ecr:CompleteLayerUpload","ecr:ListImages"],"Resource": "arn:aws:ecr:us-east-1:<ACCOUNT_ID>:repository/<REPOSITORY_NAME>"}]}EOFaws iam create-policy \--policy-name ecr-push-pull-single-repo \--policy-document file://ecr-push-pull-single-repo.json
- Determine for each user/role what they must do:
-
Attach new scoped policies and remove broad ones
- Attach the least-privilege policy to only those users/roles that require it:
aws iam attach-role-policy \--role-name <ROLE_NAME> \--policy-arn arn:aws:iam::<ACCOUNT_ID>:policy/ecr-push-pull-single-repoaws iam attach-user-policy \--user-name <USER_NAME> \--policy-arn arn:aws:iam::<ACCOUNT_ID>:policy/ecr-pull-only-single-repo
- Detach or delete overly broad policies (after verifying no dependency):
aws iam list-entities-for-policy --policy-arn <BROAD_POLICY_ARN>aws iam detach-role-policy --role-name <ROLE_NAME> --policy-arn <BROAD_POLICY_ARN>aws iam detach-user-policy --user-name <USER_NAME> --policy-arn <BROAD_POLICY_ARN># Delete only when no entities are attached:aws iam delete-policy --policy-arn <BROAD_POLICY_ARN>
- Attach the least-privilege policy to only those users/roles that require it:
-
Re-verify effective access to ECR
- Re-run the policy search to ensure no remaining broad access:
aws iam list-policies --scope Local --only-attached \--query 'Policies[].Arn' --output text | tr '\t' '\n' | while read arn; doaws iam get-policy-version \--policy-arn "$arn" \--version-id "$(aws iam get-policy --policy-arn "$arn" --query 'Policy.DefaultVersionId' --output text)" \| jq -e '.PolicyVersion.Document.Statement[]?| select((.Action|tostring|test("ecr:")) and (.Resource=="*"))' \>/dev/null && echo "Policy with broad ECR access: $arn"done
- Optionally, use IAM → Access Analyzer (AWS Console) to confirm there are no unintended principals with ECR permissions beyond what is required.
- Re-run the policy search to ensure no remaining broad access:
Using kubectl
kubectl cannot be used to change IAM user or role permissions for Amazon ECR, because those are configured at the AWS account level via IAM (console, AWS CLI, or IaC such as CloudFormation/Terraform), not through Kubernetes API objects. Please refer to the Manual Steps section for guidance on reviewing and tightening ECR-related IAM access.
Automation
#!/usr/bin/env bash
#
# Report Amazon ECR-related IAM access for review (CISEKS 5.1.2)
#
# Requirements:
# - AWS CLI v2 configured with credentials that can list IAM and ECR resources
# - jq installed
#
# Run location:
# - Any machine with AWS CLI access to the target account(s)
set -euo pipefail
ACCOUNT_ID="$(aws sts get-caller-identity --query Account --output text)"
REGION="${AWS_REGION:-${AWS_DEFAULT_REGION:-us-east-1}}"
echo "=== Context ==="
echo "Account: ${ACCOUNT_ID}"
echo "Region : ${REGION}"
echo
###############################################################################
# 1. List IAM policies that contain ECR actions
###############################################################################
echo "=== IAM customer-managed policies with ECR permissions ==="
aws iam list-policies --scope Local --no-paginate \
--query 'Policies[].Arn' --output text | tr '\t' '\n' | while read -r POLICY_ARN; do
[ -z "$POLICY_ARN" ] && continue
VERSION_ID="$(aws iam get-policy --policy-arn "$POLICY_ARN" \
--query 'Policy.DefaultVersionId' --output text)"
aws iam get-policy-version --policy-arn "$POLICY_ARN" --version-id "$VERSION_ID" \
--query '{Arn:PolicyArn,Document:PolicyVersion.Document}' --output json |
jq -r '
. as $root
| $root.Document.Statement[]
| select(
(.Action|tostring|test("ecr:", "i")) or
(try (.Resource|tostring|test("ecr", "i")) catch false)
)
| "Policy: \($root.Arn)\n Effect : \(.Effect)\n Actions: \(.Action)\n Resources: \(.Resource // "*")\n"
' || true
done
cat <<'EOF'
# REVIEW GUIDANCE (policies)
- Problematic indicators:
- Action includes overly broad patterns, e.g.:
"ecr:*"
"ecr:Get*"
"ecr:Batch*"
- Resource is "*" or all repositories:
"Resource": "*"
"arn:aws:ecr:*:<account>:repository/*"
- Policies attached to many roles/users, especially generic ones (e.g. "dev-role", "ci-role", "admin-ci").
- Preferred patterns:
- Only necessary actions (e.g. "ecr:GetAuthorizationToken", "ecr:BatchCheckLayerAvailability",
"ecr:BatchGetImage", "ecr:PutImage", "ecr:InitiateLayerUpload", "ecr:UploadLayerPart",
"ecr:CompleteLayerUpload") per use case.
- Repository-level ARNs instead of "*", e.g.:
"arn:aws:ecr:REGION:ACCOUNT:repository/app-backend"
EOF
###############################################################################
# 2. Show which roles/users/groups have ECR policies attached
###############################################################################
echo "=== IAM roles with attached policies that may include ECR permissions ==="
aws iam list-roles --no-paginate --query 'Roles[].RoleName' --output text \
| tr '\t' '\n' | while read -r ROLE; do
[ -z "$ROLE" ] && continue
ATTACHED=$(aws iam list-attached-role-policies --role-name "$ROLE" \
--query 'AttachedPolicies[].PolicyArn' --output text 2>/dev/null || true)
INLINE=$(aws iam list-role-policies --role-name "$ROLE" \
--query 'PolicyNames[]' --output text 2>/dev/null || true)
HAS_ECR=false
for P in $ATTACHED; do
VERSION_ID="$(aws iam get-policy --policy-arn "$P" \
--query 'Policy.DefaultVersionId' --output text 2>/dev/null || true)"
[ -z "$VERSION_ID" ] && continue
if aws iam get-policy-version --policy-arn "$P" --version-id "$VERSION_ID" \
--query 'PolicyVersion.Document.Statement[].Action' --output json 2>/dev/null \
| jq -e 'tostring|test("ecr:", "i")' >/dev/null 2>&1; then
HAS_ECR=true
break
fi
done
if [ "$HAS_ECR" = false ] && [ -n "$INLINE" ]; then
for NAME in $INLINE; do
if aws iam get-role-policy --role-name "$ROLE" --policy-name "$NAME" \
--query 'PolicyDocument.Statement[].Action' --output json 2>/dev/null \
| jq -e 'tostring|test("ecr:", "i")' >/dev/null 2>&1; then
HAS_ECR=true
break
fi
done
fi
if [ "$HAS_ECR" = true ]; then
echo "Role: $ROLE"
echo " Attached policies:"
for P in $ATTACHED; do
echo " - $P"
done
echo " Inline policies:"
for NAME in $INLINE; do
echo " - $NAME"
done
echo
fi
done
echo
echo "=== IAM users with attached policies that may include ECR permissions ==="
aws iam list-users --no-paginate --query 'Users[].UserName' --output text \
| tr '\t' '\n' | while read -r USER; do
[ -z "$USER" ] && continue
ATTACHED=$(aws iam list-attached-user-policies --user-name "$USER" \
--query 'AttachedPolicies[].PolicyArn' --output text 2>/dev/null || true)
INLINE=$(aws iam list-user-policies --user-name "$USER" \
--query 'PolicyNames[]' --output text 2>/dev/null || true)
HAS_ECR=false
for P in $ATTACHED; do
VERSION_ID="$(aws iam get-policy --policy-arn "$P" \
--query 'Policy.DefaultVersionId' --output text 2>/dev/null || true)"
[ -z "$VERSION_ID" ] && continue
if aws iam get-policy-version --policy-arn "$P" --version-id "$VERSION_ID" \
--query 'PolicyVersion.Document.Statement[].Action' --output json 2>/dev/null \
| jq -e 'tostring|test("ecr:", "i")' >/dev/null 2>&1; then
HAS_ECR=true
break
fi
done
if [ "$HAS_ECR" = false ] && [ -n "$INLINE" ]; then
for NAME in $INLINE; do
if aws iam get-user-policy --user-name "$USER" --policy-name "$NAME" \
--query 'PolicyDocument.Statement[].Action' --output json 2>/dev/null \
| jq -e 'tostring|test("ecr:", "i")' >/dev/null 2>&1; then
HAS_ECR=true
break
fi
done
fi
if [ "$HAS_ECR" = true ]; then
echo "User: $USER"
echo " Attached policies:"
for P in $ATTACHED; do
echo " - $P"
done
echo " Inline policies:"
for NAME in $INLINE; do
echo " - $NAME"
done
echo
fi
done
cat <<'EOF'
# REVIEW GUIDANCE (principals)
- Problematic indicators:
- Human IAM users with direct ECR access (prefer roles + SSO/federation).
- Roles used broadly (e.g. node roles, shared CI roles) with broad ECR permissions.
- Any role/user that has ECR write/delete capabilities without clear business need:
ecr:DeleteRepository*
ecr:BatchDeleteImage
ecr:PutLifecyclePolicy
ecr:SetRepositoryPolicy
ecr:ReplicateImage
EOF
###############################################################################
# 3. Enumerate repositories and their resource-based policies
###############################################################################
echo "=== ECR repositories and repository policies (resource-based) ==="
aws ecr describe-repositories --region "$REGION" --no-paginate \
--query 'repositories[].repositoryName' --output text 2>/dev/null \
| tr '\t' '\n' | while read -r REPO; do
[ -z "$REPO" ] && continue
ARN="$(aws ecr describe-repositories --region "$REGION" \
--repository-names "$REPO" \
--query 'repositories[0].repositoryArn' --output text 2>/dev/null || true)"
echo "Repository: $REPO"
echo " ARN: $ARN"
POLICY_JSON="$(aws ecr get-repository-policy --region "$REGION" \
--repository-name "$REPO" \
--query 'policyText' --output text 2>/dev/null || true || true)"
if [ -z "$POLICY_JSON" ] || [ "$POLICY_JSON" = "None" ]; then
echo " Resource policy: <none>"
else
echo " Resource policy statements granting access:"
echo "$POLICY_JSON" | jq -r '
.Statement[]
| " Effect : \(.Effect)\n Principal: \(.Principal)\n Actions : \(.Action)\n Condition: \(.Condition // "{}")\n"
'
fi
echo
done
cat <<'EOF'
# REVIEW GUIDANCE (repository policies)
- Problematic indicators:
- "Principal": "*" (public or overly broad access).
- Principals not controlled by your organization (cross-account) without explicit justification.
- "Action": "ecr:*" or similar wildcards.
- Missing restrictive "Condition" blocks when granting cross-account access.
- Preferred patterns:
- No resource policies unless explicitly required (use IAM policies instead).
- If present, tightly scoped principals and actions, with conditions (e.g. aws:PrincipalOrgID).
EOF
###############################################################################
# 4. (Optional) Mapping to EKS-related roles
###############################################################################
echo "=== EKS node and cluster roles (if present) and their attached policies ==="
aws eks list-clusters --region "$REGION" --query 'clusters[]' --output text 2>/dev/null \
| tr '\t' '\n' | while read -r CLUSTER; do
[ -z "$CLUSTER" ] && continue
echo "Cluster: $CLUSTER"
aws eks describe-cluster --name "$CLUSTER" --region "$REGION" --output json \
| jq -r '
{
clusterRoleArn: .cluster.roleArn,
nodeRoles: (try .cluster.resourcesVpcConfig.clusterSecurityGroupId catch null)
}' 2>/dev/null || true
echo " (Review IAM roles used by nodegroups / Fargate profiles separately; ensure they only have minimal ECR access.)"
echo
done
cat <<'EOF'
INTERPRETING RESULTS:
- This script does NOT change any configuration.
- Use it to identify:
- Policies with broad ECR access.
- Roles/users with ECR permissions that are unnecessary or too wide.
- Repository policies that overexpose images.
ANY OF THE FOLLOWING TYPICALLY INDICATE A PROBLEM:
- "ecr:*" in Action.
- "Resource": "*" or "repository/*".
- Public or "*" Principals in repository policies.
- Human IAM users with direct ECR push/pull when federation/roles could be used.
Next steps are manual:
- Refine policies to least privilege.
- Scope resources to specific repositories.
- Remove or tighten unnecessary repository policies.
EOF