This check flags ECR repositories whose resource policy grants public or anonymous access, meaning anyone on the internet can pull (and sometimes inspect) your container images. Lock it down by removing wildcard principals from the repository policy and scoping pulls to known accounts or IAM roles.
Container images are not just code. They carry your dependencies, base layers, build artifacts, and very often secrets that engineers swore they would clean up later. Amazon Elastic Container Registry (ECR) stores all of that, and by default it keeps your repositories private. The trouble starts when someone attaches a permissive repository policy, usually to "make the build pipeline work," and accidentally opens the registry to the entire internet.
The ecr_public check looks at every ECR repository in your account and inspects its resource-based policy for statements that grant access to anonymous or wildcard principals. If it finds one, the repository is treated as publicly accessible.
What this check detects
ECR supports a resource-based policy on each repository, similar to an S3 bucket policy. The policy controls who can perform actions like ecr:GetDownloadUrlForLayer, ecr:BatchGetImage, and ecr:BatchCheckLayerAvailability, which together are what a docker pull needs.
The check fails when a repository policy contains a statement with "Effect": "Allow" and one of the following:
"Principal": "*"— any AWS identity, authenticated or not"Principal": { "AWS": "*" }— the same thing, written differently- A statement that grants pull actions without a
Conditionrestricting the source account, organization, or VPC endpoint
Note: This is different from a public ECR repository created in the ECR Public Gallery (the public.ecr.aws registry). Those are intentionally public. This check targets private ECR repositories that have been misconfigured to allow public access through their policy.
Why it matters
A publicly readable image registry is a gift to an attacker. Here is what they can do once they find one.
1. Harvest secrets baked into images
Images frequently contain things that should never leave your perimeter: AWS access keys in .env files, database connection strings, private NPM tokens, internal CA certificates, and source code. An attacker who can pull your image can run docker history and unpack every layer offline at their leisure.
docker pull 111122223333.dkr.ecr.us-east-1.amazonaws.com/payments-api:latest
docker save payments-api -o image.tar
mkdir extracted && tar -xf image.tar -C extracted
# grep through every layer for credentials
grep -rEi "AKIA|secret|password|token" extracted/
2. Map your internal architecture
Even without secrets, an attacker learns a lot from your images: which frameworks and versions you run, internal service names, build tooling, and known-vulnerable libraries. That intelligence shortens the path to a working exploit.
3. Inflate your bill
ECR charges for data transfer out to the internet. A public repository that gets discovered and scraped repeatedly, or deliberately abused, can rack up real transfer costs.
Warning: Public read access combined with a missing or weak image scanning policy is a common precursor to supply-chain incidents. If an attacker can also push (because the policy granted write actions too), they can poison your latest tag and serve a backdoored image to every consumer of that repository.
How to fix it
The fix is to remove the public grant from the repository policy and replace it with one scoped to the specific accounts, roles, or organization that genuinely need to pull.
Step 1: Inspect the current policy
aws ecr get-repository-policy \
--repository-name payments-api \
--region us-east-1 \
--query 'policyText' --output text | jq .
Look for any statement with a wildcard principal:
{
"Version": "2008-10-17",
"Statement": [
{
"Sid": "AllowPull",
"Effect": "Allow",
"Principal": "*",
"Action": [
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:BatchCheckLayerAvailability"
]
}
]
}
Step 2: Replace it with a scoped policy
Decide who actually needs access. If only your own accounts pull these images, grant access to those account IDs. Write a corrected policy to a file:
{
"Version": "2008-10-17",
"Statement": [
{
"Sid": "AllowPullFromKnownAccounts",
"Effect": "Allow",
"Principal": {
"AWS": [
"arn:aws:iam::444455556666:root",
"arn:aws:iam::777788889999:root"
]
},
"Action": [
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:BatchCheckLayerAvailability"
]
}
]
}
Danger: Applying a new repository policy overwrites the existing one entirely. Before you run the command below, confirm your replacement policy includes every legitimate principal that was relying on the old policy, or you will break running pipelines and deployments that pull this image.
Step 3: Apply the corrected policy
aws ecr set-repository-policy \
--repository-name payments-api \
--region us-east-1 \
--policy-text file://ecr-policy.json
If the repository never needed a cross-account policy in the first place, delete the policy entirely so it falls back to IAM-only access within your account:
aws ecr delete-repository-policy \
--repository-name payments-api \
--region us-east-1
Restrict by organization instead of listing accounts
If you operate many accounts under AWS Organizations, listing every account ID is brittle. Use a condition on aws:PrincipalOrgID instead:
{
"Version": "2008-10-17",
"Statement": [
{
"Sid": "AllowPullFromMyOrg",
"Effect": "Allow",
"Principal": "*",
"Action": [
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:BatchCheckLayerAvailability"
],
"Condition": {
"StringEquals": {
"aws:PrincipalOrgID": "o-abc123def4"
}
}
}
]
}
Note: A "Principal": "*" is only safe when it is paired with a tight Condition. The wildcard principal here is constrained to identities in your organization, so it is not public. Lensix treats a wildcard principal as a finding only when there is no scoping condition.
How to prevent it from happening again
Manual fixes do not stick. The same misconfiguration reappears the next time someone is debugging a cross-account pull at 2 a.m. Push the guardrail left into your IaC and CI/CD.
Define the policy in Terraform
Codify the repository policy so that it lives in review and cannot drift silently.
resource "aws_ecr_repository" "payments_api" {
name = "payments-api"
image_tag_mutability = "IMMUTABLE"
image_scanning_configuration {
scan_on_push = true
}
}
resource "aws_ecr_repository_policy" "payments_api" {
repository = aws_ecr_repository.payments_api.name
policy = jsonencode({
Version = "2008-10-17"
Statement = [{
Sid = "AllowPullFromMyOrg"
Effect = "Allow"
Principal = "*"
Action = [
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:BatchCheckLayerAvailability"
]
Condition = {
StringEquals = {
"aws:PrincipalOrgID" = "o-abc123def4"
}
}
}]
})
}
Block public policies at the source with a CI gate
Add a static analysis step that fails the build if any ECR policy grants an unconditioned wildcard. Tools like Checkov or tfsec catch this in plan stage. A minimal Checkov run in CI looks like this:
checkov -d . --framework terraform \
--check CKV_AWS_32 \
--compact
Tip: Pair the CI check with a Service Control Policy at the Organizations level that denies ecr:SetRepositoryPolicy for anyone except your platform team. That way even a console user with broad ECR permissions cannot open a repository to the world.
Continuous detection with Lensix
IaC checks only cover resources created through IaC. Click-ops changes, emergency hotfixes, and acquired accounts slip past them. Lensix runs ecr_public continuously across all your AWS accounts and regions, so a repository opened up after deployment is caught within minutes rather than at the next audit.
Best practices
- Default to private and IAM-only. Most repositories do not need a resource policy at all. Cross-account access is the exception, not the rule.
- Never use a bare wildcard principal without a condition. If you see
"Principal": "*"without anaws:PrincipalOrgID,aws:SourceVpce, or account condition, treat it as public. - Separate pull from push. Build pipelines push, runtime workloads pull. Grant each only the actions it needs rather than handing out broad ECR access.
- Enable scan-on-push and image immutability. Scanning surfaces vulnerable layers, and immutable tags stop an attacker from quietly replacing
latest. - Keep secrets out of images entirely. Use IAM roles, instance profiles, or a secrets manager at runtime. A leaked image should not be a leaked credential.
- Use VPC endpoints for ECR. Pulling images over a PrivateLink endpoint keeps traffic off the public internet and lets you condition policies on
aws:SourceVpce. - Review repository policies during access reviews. Account IDs in a policy go stale as projects shut down. Prune principals that no longer exist.
The core idea is simple: a private registry should stay private, and the only path to "public" should be a deliberate, reviewed decision backed by a tight condition. Everything in this post is about making the safe choice the default and making the unsafe choice loud and visible.

