DevSecOps Guides

DevSecOps Guides

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

Reza's avatar
Reza
Apr 10, 2026
∙ Paid

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

  1. ECR Repository Policy Allowing Public Access

  2. ECR Private Registry Permissions Policy

  3. ECR Cross-Account Access Misconfiguration

  4. ECR Without VPC Endpoint (PrivateLink)

ECR Image Scanning

  1. ECR Basic Scanning vs Enhanced Scanning

  2. ECR Scan on Push Disabled

  3. Amazon Inspector ECR Deep Integration

  4. ECR Vulnerability Findings and Remediation Workflow

ECR Image Lifecycle and Tags

  1. ECR Missing Lifecycle Policies

  2. ECR Image Tag Mutability

  3. ECR Encryption with KMS

  4. ECR CloudTrail Audit Logging

ECR with CI/CD

  1. ECR with GitHub Actions (OIDC Federation)

  2. ECR with GitLab CI Pipeline

  3. ECR Infrastructure with Terraform

  4. ECR Pull-Through Cache Security

ECR Image Signing

  1. ECR Replication (Cross-Region and Cross-Account)

  2. ECR Image Signing with AWS Signer and Notation

  3. ECR Image Signing with Cosign

  4. ECR with EKS using IRSA

ECR with Compute

  1. ECR with ECS and Fargate

  2. ECR with Lambda Container Images

ECR Advanced Features

  1. ECR OCI Artifact Support

  2. ECR SBOM Generation with Amazon Inspector

ECR Governance and Architecture

  1. ECR with AWS Config Rules

  2. ECR Findings in AWS Security Hub

  3. ECR Public vs Private Repository Security

  4. ECR Image Layer Analysis and Security

  5. ECR Admission Control with Kyverno on EKS

  6. ECR Security Audit with Prowler

  7. ECR Disaster Recovery and Backup

  8. 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

Lab 02: ECR Private Registry Permissions Policy

User's avatar

Continue reading this post for free, courtesy of Reza.

Or purchase a paid subscription.
© 2026 Reza · Privacy ∙ Terms ∙ Collection notice
Start your SubstackGet the app
Substack is the home for great culture