AWS ECR Image Scanning: Vulnerability Detection, Trivy CI, SBOM, and Image Signing with Cosign
Secure your container images with AWS ECR enhanced scanning, Trivy in CI/CD pipelines, Software Bill of Materials (SBOM) generation, and Cosign image signing. Covers Inspector integration, CVE reporting, and Terraform setup.
Shipping a container image without scanning it is like deploying code without running tests. CVEs in base images and third-party packages are a common attack vector — and they're fixable before deployment. AWS ECR provides two scanning tiers, Trivy adds CI-gate capability, and Cosign ensures images haven't been tampered with between build and deploy.
This guide covers the full supply chain security stack for containers.
ECR Scanning: Basic vs Enhanced
| Feature | Basic Scanning | Enhanced Scanning |
|---|---|---|
| Engine | Clair (open source) | Amazon Inspector + Snyk |
| OS packages | ✅ | ✅ |
| Language packages (npm, pip, go) | ❌ | ✅ |
| Scan on push | ✅ | ✅ |
| Continuous scanning (after push) | ❌ | ✅ (rescans as new CVEs discovered) |
| Cost | Free | $0.09/image/month after first 500 |
| EventBridge integration | ✅ | ✅ |
Terraform: ECR with Enhanced Scanning
# terraform/ecr.tf
# Enable enhanced scanning at the registry level
resource "aws_ecr_registry_scanning_configuration" "main" {
scan_type = "ENHANCED" # Or "BASIC"
rule {
scan_frequency = "SCAN_ON_PUSH"
repository_filter {
filter = "*" # Scan all repos
filter_type = "WILDCARD"
}
}
rule {
scan_frequency = "CONTINUOUS_SCAN" # Enhanced only
repository_filter {
filter = "prod-*" # Continuous scan for production images
filter_type = "WILDCARD"
}
}
}
resource "aws_ecr_repository" "app" {
name = "${var.app_name}-app"
image_tag_mutability = "IMMUTABLE" # Prevent tag overwriting (security baseline)
image_scanning_configuration {
scan_on_push = true
}
encryption_configuration {
encryption_type = "KMS"
kms_key = aws_kms_key.ecr.arn
}
tags = local.tags
}
# Lifecycle policy: keep last 10 tagged images, expire untagged after 7 days
resource "aws_ecr_lifecycle_policy" "app" {
repository = aws_ecr_repository.app.name
policy = jsonencode({
rules = [
{
rulePriority = 1
description = "Keep last 10 production images"
selection = {
tagStatus = "tagged"
tagPrefixList = ["v"]
countType = "imageCountMoreThan"
countNumber = 10
}
action = { type = "expire" }
},
{
rulePriority = 2
description = "Expire untagged images after 7 days"
selection = {
tagStatus = "untagged"
countType = "sinceImagePushed"
countUnit = "days"
countNumber = 7
}
action = { type = "expire" }
}
]
})
}
# Alert when CRITICAL vulnerabilities are found
resource "aws_cloudwatch_event_rule" "ecr_critical_cve" {
name = "${var.app_name}-ecr-critical-cve"
event_pattern = jsonencode({
source = ["aws.inspector2"]
detail-type = ["Inspector2 Finding"]
detail = {
severity = [{ exists = true }]
resources = {
type = ["AWS_ECR_CONTAINER_IMAGE"]
}
severity = ["CRITICAL"]
}
})
}
resource "aws_cloudwatch_event_target" "ecr_cve_sns" {
rule = aws_cloudwatch_event_rule.ecr_critical_cve.name
arn = aws_sns_topic.security_alerts.arn
}
☁️ Is Your Cloud Costing Too Much?
Most teams overspend 30–40% on cloud — wrong instance types, no reserved pricing, bloated storage. We audit, right-size, and automate your infrastructure.
- AWS, GCP, Azure certified engineers
- Infrastructure as Code (Terraform, CDK)
- Docker, Kubernetes, GitHub Actions CI/CD
- Typical audit recovers $500–$3,000/month in savings
Trivy in CI: Block on Critical CVEs
# .github/workflows/build-and-scan.yml
name: Build, Scan, and Push
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
id-token: write # For OIDC → AWS
security-events: write # For SARIF upload to GitHub Security
env:
ECR_REGISTRY: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com
IMAGE_NAME: ${{ secrets.APP_NAME }}-app
jobs:
build-scan-push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Build image with build args
- name: Build Docker image
run: |
docker build \
--build-arg BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) \
--build-arg GIT_SHA=${{ github.sha }} \
--tag $IMAGE_NAME:${{ github.sha }} \
--tag $IMAGE_NAME:latest \
.
# Scan with Trivy — fail on CRITICAL CVEs
- name: Scan with Trivy (fail on CRITICAL)
uses: aquasecurity/trivy-action@master
with:
image-ref: "${{ env.IMAGE_NAME }}:${{ github.sha }}"
format: "table"
exit-code: "1" # Fail CI on vulnerabilities found
severity: "CRITICAL" # Only fail on CRITICAL (not HIGH)
ignore-unfixed: true # Skip CVEs with no fix available
# Also output SARIF for GitHub Security tab
- name: Scan with Trivy (SARIF output)
uses: aquasecurity/trivy-action@master
with:
image-ref: "${{ env.IMAGE_NAME }}:${{ github.sha }}"
format: "sarif"
output: "trivy-results.sarif"
severity: "HIGH,CRITICAL"
ignore-unfixed: true
- name: Upload SARIF to GitHub Security
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy-results.sarif
# Only push if scan passes
- name: Configure AWS credentials (OIDC)
if: github.ref == 'refs/heads/main'
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
aws-region: ${{ secrets.AWS_REGION }}
- name: Login to ECR
if: github.ref == 'refs/heads/main'
uses: aws-actions/amazon-ecr-login@v2
- name: Push to ECR
if: github.ref == 'refs/heads/main'
run: |
docker tag $IMAGE_NAME:${{ github.sha }} \
$ECR_REGISTRY/$IMAGE_NAME:${{ github.sha }}
docker tag $IMAGE_NAME:${{ github.sha }} \
$ECR_REGISTRY/$IMAGE_NAME:latest
docker push $ECR_REGISTRY/$IMAGE_NAME:${{ github.sha }}
docker push $ECR_REGISTRY/$IMAGE_NAME:latest
SBOM Generation with Syft
A Software Bill of Materials lists every package in your image — required for compliance in many industries.
# After build, before push
- name: Generate SBOM with Syft
uses: anchore/sbom-action@v0
with:
image: "${{ env.IMAGE_NAME }}:${{ github.sha }}"
format: spdx-json # Or cyclonedx-json
output-file: sbom.spdx.json
# Upload SBOM as build artifact
- name: Upload SBOM artifact
uses: actions/upload-artifact@v4
with:
name: sbom-${{ github.sha }}
path: sbom.spdx.json
retention-days: 90
# Optionally: attach SBOM to ECR image
- name: Attach SBOM to ECR image
run: |
oras attach \
--artifact-type application/vnd.syft+json \
$ECR_REGISTRY/$IMAGE_NAME:${{ github.sha }} \
sbom.spdx.json
⚙️ DevOps Done Right — Zero Downtime, Full Automation
Ship faster without breaking things. We build CI/CD pipelines, monitoring stacks, and auto-scaling infrastructure that your team can actually maintain.
- Staging + production environments with feature flags
- Automated security scanning in the pipeline
- Uptime monitoring + alerting + runbook automation
- On-call support handover docs included
Image Signing with Cosign
Cosign ensures that the image deployed to ECS/EKS is exactly what was built in CI — no tampering in transit.
# Install Cosign
- name: Install Cosign
uses: sigstore/cosign-installer@v3
# Sign image with keyless signing (OIDC-based, no private key to manage)
- name: Sign image with Cosign (keyless)
if: github.ref == 'refs/heads/main'
env:
COSIGN_EXPERIMENTAL: "1"
run: |
cosign sign --yes \
$ECR_REGISTRY/$IMAGE_NAME:${{ github.sha }}
# Verify before deployment (in deploy job or admission webhook)
- name: Verify image signature
if: github.ref == 'refs/heads/main'
env:
COSIGN_EXPERIMENTAL: "1"
run: |
cosign verify \
--certificate-identity-regexp="https://github.com/${{ github.repository }}.*" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
$ECR_REGISTRY/$IMAGE_NAME:${{ github.sha }}
Dockerfile Best Practices for Low CVE Surface
# Use minimal base images — fewer packages = fewer CVEs
FROM node:22-alpine AS base
# ✅ Alpine base images have ~50 packages vs ~200 in debian-slim
# ✅ Always pin to a specific digest for reproducibility
# FROM node:22-alpine@sha256:<digest>
# Don't run as root
RUN addgroup -S app && adduser -S app -G app
# Build stage
FROM base AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --ignore-scripts # --ignore-scripts: prevents malicious postinstall scripts
COPY . .
RUN npm run build
# Production stage
FROM base AS runner
WORKDIR /app
# Only copy production artifacts
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
# Drop to non-root user
USER app
# Health check for container orchestrators
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget -qO- http://localhost:3000/api/health || exit 1
EXPOSE 3000
CMD ["node", "server.js"]
Check Scan Results via AWS CLI
# Get scan findings for the latest image in a repository
aws ecr describe-image-scan-findings \
--repository-name myapp-app \
--image-id imageTag=latest \
--query 'imageScanFindings.findingSeverityCounts' \
--output table
# Output example:
# +-----------+-------+
# | CRITICAL | 0 |
# | HIGH | 3 |
# | MEDIUM | 12 |
# | LOW | 24 |
# | INFO | 8 |
# +-----------+-------+
# List CRITICAL findings with CVE IDs
aws ecr describe-image-scan-findings \
--repository-name myapp-app \
--image-id imageTag=latest \
--query 'imageScanFindings.findings[?severity==`CRITICAL`].[name,description,uri]' \
--output table
Cost Estimates
| Component | Cost |
|---|---|
| ECR Basic scanning | Free |
| ECR Enhanced scanning (Amazon Inspector) | $0.09/image/month after first 500 |
| Trivy in CI | Free (open source) |
| Cosign keyless signing | Free (Sigstore public good) |
| SBOM storage (S3) | ~$0.023/GB/month |
| CloudWatch Events + SNS alerts | ~$1–2/month |
See Also
- AWS ECS Fargate Production Setup
- AWS IAM Least Privilege
- Docker Multi-Stage Builds
- AWS ECS Blue-Green Deployment
- Terraform State Management
Working With Viprasol
Container vulnerabilities are preventable but require deliberate tooling. Our team sets up ECR with enhanced scanning, blocks CI on CRITICAL CVEs with Trivy, generates SBOM artifacts for compliance, and signs images with Cosign so deployments can verify image provenance before pulling.
What we deliver:
- ECR Terraform with
IMMUTABLEtags, KMS encryption, lifecycle policy (10 tagged, 7-day untagged expiry) - Enhanced scanning with CONTINUOUS_SCAN for production repositories
- Trivy GitHub Action:
exit-code: 1on CRITICAL + SARIF upload to GitHub Security tab - Multi-stage Dockerfile: Alpine base,
--ignore-scriptsnpm install, non-root USER - Syft SBOM generation in SPDX-JSON format, uploaded as build artifact
- Cosign keyless signing + verification step before deploy
Talk to our team about container security for your platform →
Or explore our cloud infrastructure services.
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
Need DevOps & Cloud Expertise?
Scale your infrastructure with confidence. AWS, GCP, Azure certified team.
Free consultation • No commitment • Response within 24 hours
Making sense of your data at scale?
Viprasol builds end-to-end big data analytics solutions — ETL pipelines, data warehouses on Snowflake or BigQuery, and self-service BI dashboards. One reliable source of truth for your entire organisation.