Skip to main content

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​

Manual Steps
  1. Inventory who can access ECR

    • On any machine with AWS CLI configured for the account:
      aws ecr describe-repositories --region us-east-1
      aws iam list-users
      aws iam list-roles
    • In the AWS Console: IAM → Access Analyzer → Analyze policies (optional) to see which principals have ECR access.
  2. 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; do
      aws 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>
  3. 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).
    • Use console: IAM → Policies → (select policy) → JSON tab; verify actions and resources are narrowed to required repositories and operations.
  4. 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.
    • 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>"
      }
      ]
      }
      EOF
      aws iam create-policy \
      --policy-name ecr-push-pull-single-repo \
      --policy-document file://ecr-push-pull-single-repo.json
  5. 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-repo

      aws 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>
  6. 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; do
      aws 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.
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