AWS ECR Security Labs in 2026
We wrote 32 hands-on labs covering every security control for AWS Elastic Container Registry. Each lab walks through a real misconfiguration we find during AWS assessments, shows what an attacker does
Before we start, shoutout to a platform we built for YOU!
💎 Your next level in cybersecurity isn’t a dream, it’s a proactive roadmap.
HADESS AI Career Coach turns ambition into expertise:
→ 390+ clear career blueprints from entry-level to leadership
→ 490+ in-demand skill modules + practical labs
→ Intelligent AI(Not AI buzz, applied AI, promise!) tools + real-world expert coaches and scenarios
Master the skills that matter. Land the roles that pay. Build the future you want.
🔥 Start engineering your career →
https://career.hadess.io
The labs span eight areas: ECR access control and policies (repository policies, registry permissions, cross-account access, VPC endpoints), image scanning (basic vs enhanced, scan-on-push, Amazon Inspector integration, remediation workflows), image lifecycle and tags (lifecycle policies, immutable tags, KMS encryption, CloudTrail logging), CI/CD pipelines (GitHub Actions OIDC, GitLab CI, Terraform, pull-through cache), image signing (AWS Signer with Notation, Cosign with AWS KMS), compute integration (EKS with IRSA, ECS/Fargate, Lambda container images), advanced features (replication, OCI artifacts, SBOM with Inspector), and governance (Config rules, Security Hub, Prowler audit, complete security architecture).
Every lab uses real AWS CLI commands, real Terraform configurations, and real scanning tools. No fake output. No tools that do not exist.
Table of Contents
ECR Access Control and Policies
ECR Repository Policy Allowing Public Access
ECR Private Registry Permissions Policy
ECR Cross-Account Access Misconfiguration
ECR Without VPC Endpoint (PrivateLink)
ECR Image Scanning
ECR Basic Scanning vs Enhanced Scanning
ECR Scan on Push Disabled
Amazon Inspector ECR Deep Integration
ECR Vulnerability Findings and Remediation Workflow
ECR Image Lifecycle and Tags
ECR Missing Lifecycle Policies
ECR Image Tag Mutability
ECR Encryption with KMS
ECR CloudTrail Audit Logging
ECR with CI/CD
ECR with GitHub Actions (OIDC Federation)
ECR with GitLab CI Pipeline
ECR Infrastructure with Terraform
ECR Pull-Through Cache Security
ECR Image Signing
ECR Replication (Cross-Region and Cross-Account)
ECR Image Signing with AWS Signer and Notation
ECR Image Signing with Cosign
ECR with EKS using IRSA
ECR with Compute
ECR with ECS and Fargate
ECR with Lambda Container Images
ECR Advanced Features
ECR OCI Artifact Support
ECR SBOM Generation with Amazon Inspector
ECR Governance and Architecture
ECR with AWS Config Rules
ECR Findings in AWS Security Hub
ECR Public vs Private Repository Security
ECR Image Layer Analysis and Security
ECR Admission Control with Kyverno on EKS
ECR Security Audit with Prowler
ECR Disaster Recovery and Backup
Complete ECR Security Architecture
Lab 01: ECR Repository Policy Allowing Public Access
When an ECR repository policy sets Principal: "*", it grants every AWS account (and in some configurations, unauthenticated callers) permission to pull images. This is the container equivalent of making an S3 bucket public. If an attacker discovers the account ID and repository name, they can pull every image in that repository without any authentication beyond a valid AWS credential set.
We see this happen frequently in organizations that needed to share a container image with a partner and took the shortcut of opening the policy to everyone. The temporary fix becomes permanent, and proprietary application code, configuration files, secrets baked into image layers, and internal tooling all become accessible to anyone.
The attack surface is larger than people expect. ECR repository names are often guessable (e.g., my-company/backend-api, production/web-app). AWS account IDs leak through CloudTrail logs, error messages, IAM role ARNs in public repositories, and S3 bucket policies. Once the attacker has both pieces of information, pulling the image is trivial.
Root Cause Analysis
The root cause is a repository policy with an overly permissive principal. The policy below grants ecr:GetDownloadUrlForLayer, ecr:BatchGetImage, and ecr:BatchCheckLayerAvailability to "*", which means any AWS principal. There is no condition block to restrict access by account, organization, IP range, or VPC endpoint.
Teams often apply this policy during development and forget to scope it down. Infrastructure-as-code reviews miss it because the repository “works” and nobody validates the principal field against organizational policy.
Vulnerable Configuration
AWS CLI (set-repository-policy)
aws ecr set-repository-policy \
--repository-name my-company/backend-api \
--policy-text '{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowPublicPull",
"Effect": "Allow",
"Principal": "*",
"Action": [
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:BatchCheckLayerAvailability"
]
}
]
}'
Terraform
resource "aws_ecr_repository" "backend_api" {
name = "my-company/backend-api"
}
resource "aws_ecr_repository_policy" "backend_api_policy" {
repository = aws_ecr_repository.backend_api.name
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AllowPublicPull"
Effect = "Allow"
Principal = "*"
Action = [
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:BatchCheckLayerAvailability"
]
}
]
})
}
Exploitation
Step 1: Discover the target account ID and repository name
The attacker already obtained the account ID 111122223333 from a leaked CloudTrail log. They guess or enumerate common repository names.
# Attacker authenticates to ECR using their own AWS credentials
# but targets the victim's registry
aws ecr get-login-password --region us-east-1 | \
docker login --username AWS --password-stdin 111122223333.dkr.ecr.us-east-1.amazonaws.com
Expected output:
Login Succeeded
Step 2: Pull the image from the victim’s repository
docker pull 111122223333.dkr.ecr.us-east-1.amazonaws.com/my-company/backend-api:latest
Expected output:
latest: Pulling from my-company/backend-api
a2abf6c4d29d: Pull complete
e1a9e1e234bc: Pull complete
05e4bd1f585e: Pull complete
Digest: sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
Status: Downloaded newer image for 111122223333.dkr.ecr.us-east-1.amazonaws.com/my-company/backend-api:latest
Step 3: Extract secrets and proprietary code from image layers
# Inspect the image for environment variables, config files, embedded secrets
docker history 111122223333.dkr.ecr.us-east-1.amazonaws.com/my-company/backend-api:latest
docker run --rm -it --entrypoint /bin/sh \
111122223333.dkr.ecr.us-east-1.amazonaws.com/my-company/backend-api:latest \
-c "env; cat /app/config/*.yml"
Expected output (example):
DATABASE_URL=postgres://admin:P@ssw0rd123@prod-db.internal:5432/myapp
API_SECRET_KEY=sk-live-abc123def456
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
Step 4: Use extracted credentials for lateral movement
The attacker now has database credentials, API keys, and possibly AWS credentials embedded in the image. These can be used to access production databases, internal APIs, and other AWS services.
Detection
AWS CLI: Check repository policy
aws ecr get-repository-policy \
--repository-name my-company/backend-api \
--query 'policyText' --output text | jq .
Look for "Principal": "*" or "Principal": {"AWS": "*"} in the output.
List all repositories and check each policy
for repo in $(aws ecr describe-repositories --query 'repositories[].repositoryName' --output text); do
echo "=== $repo ==="
aws ecr get-repository-policy --repository-name "$repo" 2>/dev/null | \
jq -r '.policyText' | jq 'select(.Statement[].Principal == "*")'
done
Prowler
prowler aws --check ecr-repositories-not-publicly-accessible -r us-east-1
Expected output when vulnerable:
FAIL ECR repository my-company/backend-api has a policy that allows public access
Checkov
checkov -d . --check CKV_AWS_163
Expected output when vulnerable:
Check: CKV_AWS_163: "Ensure ECR repository policy is not set to public"
FAILED for resource: aws_ecr_repository_policy.backend_api_policy
File: /main.tf:10-28
ScoutSuite
scout suite aws --services ecr
Review the report under ECR > Findings for public repository policies.
CloudTrail: Detect external pulls
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=BatchGetImage \
--max-results 20 \
--query 'Events[].{Time:EventTime,User:Username,Source:CloudTrailEvent}' \
--output table
Look for BatchGetImage calls from account IDs that are not in your organization.
Solution
Fixed Repository Policy (AWS CLI)
aws ecr set-repository-policy \
--repository-name my-company/backend-api \
--policy-text '{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowSpecificAccountPull",
"Effect": "Allow",
"Principal": {
"AWS": [
"arn:aws:iam::444455556666:root",
"arn:aws:iam::777788889999:role/ECSTaskRole"
]
},
"Action": [
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:BatchCheckLayerAvailability"
],
"Condition": {
"StringEquals": {
"aws:PrincipalOrgID": "o-abc123def4"
}
}
}
]
}'
Fixed Terraform
resource "aws_ecr_repository_policy" "backend_api_policy" {
repository = aws_ecr_repository.backend_api.name
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AllowSpecificAccountPull"
Effect = "Allow"
Principal = {
AWS = [
"arn:aws:iam::444455556666:root",
"arn:aws:iam::777788889999:role/ECSTaskRole"
]
}
Action = [
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:BatchCheckLayerAvailability"
]
Condition = {
StringEquals = {
"aws:PrincipalOrgID" = "o-abc123def4"
}
}
}
]
})
}
Delete the policy entirely (if no cross-account access is needed)
aws ecr delete-repository-policy --repository-name my-company/backend-api
Verification
Confirm the policy is scoped
aws ecr get-repository-policy \
--repository-name my-company/backend-api \
--query 'policyText' --output text | \
jq '.Statement[].Principal'
Expected output (should NOT contain "*"):
{
"AWS": [
"arn:aws:iam::444455556666:root",
"arn:aws:iam::777788889999:role/ECSTaskRole"
]
}
Re-run Prowler
prowler aws --check ecr-repositories-not-publicly-accessible -r us-east-1
Expected output:
PASS ECR repository my-company/backend-api does not allow public access
Re-run Checkov
checkov -d . --check CKV_AWS_163
Expected output:
Check: CKV_AWS_163: "Ensure ECR repository policy is not set to public"
PASSED for resource: aws_ecr_repository_policy.backend_api_policy
Test that unauthorized accounts are denied
# From an account NOT in the policy
aws ecr get-download-url-for-layer \
--repository-name my-company/backend-api \
--registry-id 111122223333 \
--layer-digest sha256:abc123
# Expected: AccessDeniedException



