AWS ECR Container Security: Image Scanning, Signing, and Access Controls
Container images are a critical link in the software supply chain. A compromised base image, an unpatched vulnerability, or an unauthorized image push can propagate through every deployment. ECR provides the tools to secure this supply chain — but they require deliberate configuration.
Enhanced Image Scanning
ECR offers two scanning modes: basic scanning (Clair-based OS vulnerability detection) and enhanced scanning (Amazon Inspector-powered, covering both OS packages and programming language libraries).
Configuring Enhanced Scanning
# Enable enhanced scanning at the registry level
aws ecr put-registry-scanning-configuration \
--scan-type ENHANCED \
--rules '[{
"repositoryFilters": [{"filter": "*", "filterType": "WILDCARD"}],
"scanFrequency": "SCAN_ON_PUSH"
}]'
# Verify scanning configuration
aws ecr get-registry-scanning-configuration \
--query 'scanningConfiguration.{Type: scanType, Rules: rules}'
Enhanced scanning detects vulnerabilities in:
- OS packages — Debian, Ubuntu, Amazon Linux, Alpine package vulnerabilities
- Language packages — Python (pip), Node.js (npm), Java (Maven), Go modules, Ruby (gems)
- Application dependencies — transitive dependency vulnerabilities that your code inherits
Querying Scan Results
import boto3
from datetime import datetime
def get_critical_findings(repository_name, image_tag='latest'):
"""Get critical and high severity findings for an image"""
ecr = boto3.client('ecr')
inspector = boto3.client('inspector2')
# Get the image digest
images = ecr.describe_images(
repositoryName=repository_name,
imageIds=[{'imageTag': image_tag}]
)
digest = images['imageDetails'][0]['imageDigest']
account_id = boto3.client('sts').get_caller_identity()['Account']
region = boto3.session.Session().region_name
# Query Inspector findings for this image
response = inspector.list_findings(
filterCriteria={
'ecrImageHash': [{'comparison': 'EQUALS', 'value': digest}],
'severity': [
{'comparison': 'EQUALS', 'value': 'CRITICAL'},
{'comparison': 'EQUALS', 'value': 'HIGH'}
]
},
sortCriteria={'field': 'SEVERITY', 'sortOrder': 'DESC'}
)
findings = []
for finding in response['findings']:
findings.append({
'severity': finding['severity'],
'title': finding['title'],
'package': finding.get('packageVulnerabilityDetails', {}).get('vulnerablePackages', [{}])[0].get('name', 'unknown'),
'fixed_version': finding.get('packageVulnerabilityDetails', {}).get('vulnerablePackages', [{}])[0].get('fixedInVersion', 'no fix available'),
'description': finding['description'][:200]
})
return {
'repository': repository_name,
'image_tag': image_tag,
'digest': digest[:16],
'scan_time': datetime.now().isoformat(),
'critical_count': sum(1 for f in findings if f['severity'] == 'CRITICAL'),
'high_count': sum(1 for f in findings if f['severity'] == 'HIGH'),
'findings': findings
}
Integrate this into your CI/CD pipeline to gate deployments. A common policy: block deployment if any CRITICAL findings exist, warn on HIGH.
Repository Policies
Repository policies control who can push and pull images. The default allows any authenticated principal in the account full access — this is almost always too broad.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCICDPush",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/cicd-pipeline-role"
},
"Action": [
"ecr:BatchCheckLayerAvailability",
"ecr:CompleteLayerUpload",
"ecr:InitiateLayerUpload",
"ecr:PutImage",
"ecr:UploadLayerPart"
]
},
{
"Sid": "AllowECSPull",
"Effect": "Allow",
"Principal": {
"AWS": [
"arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
"arn:aws:iam::987654321098:role/ecsTaskExecutionRole"
]
},
"Action": [
"ecr:BatchGetImage",
"ecr:GetDownloadUrlForLayer"
]
},
{
"Sid": "DenyAllOthers",
"Effect": "Deny",
"Principal": "*",
"Action": "ecr:*",
"Condition": {
"StringNotLike": {
"aws:PrincipalArn": [
"arn:aws:iam::123456789012:role/cicd-pipeline-role",
"arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
"arn:aws:iam::987654321098:role/ecsTaskExecutionRole",
"arn:aws:iam::123456789012:role/admin-*"
]
}
}
}
]
}
Key principles:
- Push access limited to CI/CD — only the pipeline role can write images
- Pull access limited to runtime roles — only ECS task execution roles, EKS node roles, or Lambda can pull
- Explicit deny for everyone else — defense-in-depth against overpermissive identity policies
Lifecycle Policies
Untagged and old images accumulate quickly. Lifecycle policies automatically clean them up:
{
"rules": [
{
"rulePriority": 1,
"description": "Expire untagged images after 1 day",
"selection": {
"tagStatus": "untagged",
"countType": "sinceImagePushed",
"countUnit": "days",
"countNumber": 1
},
"action": {"type": "expire"}
},
{
"rulePriority": 2,
"description": "Keep only the 20 most recent tagged images",
"selection": {
"tagStatus": "tagged",
"tagPatternList": ["*"],
"countType": "imageCountMoreThan",
"countNumber": 20
},
"action": {"type": "expire"}
},
{
"rulePriority": 3,
"description": "Keep production tags for 90 days",
"selection": {
"tagStatus": "tagged",
"tagPatternList": ["prod-*", "release-*"],
"countType": "sinceImagePushed",
"countUnit": "days",
"countNumber": 90
},
"action": {"type": "expire"}
}
]
}
Fewer images means fewer potential targets for exploitation and lower storage costs.
Image Signing
Image signing ensures that only images built by your CI/CD pipeline can be deployed. Use Notation (the CNCF standard) or cosign:
# Sign an image after successful build and scan
IMAGE_URI="123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:v1.2.3"
# Using cosign with AWS KMS
cosign sign --key awskms:///arn:aws:kms:us-east-1:123456789012:key/abc-123 $IMAGE_URI
# Verify the signature before deployment
cosign verify --key awskms:///arn:aws:kms:us-east-1:123456789012:key/abc-123 $IMAGE_URI
In a complete pipeline: build, scan, test, sign. Then configure your orchestrator (ECS, EKS) to verify signatures before running containers. On EKS, use admission controllers like Kyverno or OPA Gatekeeper to enforce signature verification.
Pull-Through Cache for Base Images
Instead of pulling base images directly from Docker Hub or other registries (risking rate limits and supply chain attacks), use ECR pull-through cache:
# Create a pull-through cache rule for Docker Hub
aws ecr create-pull-through-cache-rule \
--ecr-repository-prefix docker-hub \
--upstream-registry-url registry-1.docker.io \
--credential-arn arn:aws:secretsmanager:us-east-1:123456789012:secret:dockerhub-creds
# Now pull through ECR instead of Docker Hub directly
docker pull 123456789012.dkr.ecr.us-east-1.amazonaws.com/docker-hub/library/python:3.12-slim
ECR caches the image locally, scans it automatically, and serves subsequent pulls from the cache. This protects against Docker Hub outages, rate limits, and compromised upstream images.
Securing Container Images with AccessLens
Container security extends beyond the image itself. The IAM roles that push, pull, and run containers determine who can access your container supply chain and what those containers can do at runtime.
AccessLens helps secure your container workloads by providing:
- ECR access analysis that maps every principal with push or pull access to each repository
- Task role visibility that shows the effective permissions of ECS task roles and EKS pod roles
- Cross-account image sharing audit that reveals which accounts can pull your production images
- CI/CD role analysis that identifies overpermissive pipeline roles with broad ECR access
Your container images are only as secure as the IAM policies that control access to them. AccessLens gives you the visibility to enforce least privilege across your container supply chain.
Secure your container supply chain with AccessLens and ensure that only authorized principals can push, pull, and run your container images.