Container Registry Security Testing: Image Scanning, Supply Chain Attacks, and Registry Misconfiguration

Container registries are a critical link in the software supply chain. Misconfigured registries expose images to public access; insecure image builds bake in secrets; unsigned images enable supply chain substitution attacks. This guide covers the full container registry attack surface: Docker Hub and private registry misconfiguration, layer-by-layer secret extraction, ECR/GCR/ACR-specific vulnerabilities, image signing bypass with cosign, and registry API exploitation.

Table of Contents

  1. Registry Security Landscape
  2. Image Layer Secret Extraction
  3. Registry API Exploitation
  4. Cloud Registry Misconfigurations (ECR/GCR/ACR)
  5. Supply Chain Attack Vectors
  6. Vulnerability Scanning with Trivy and Grype
  7. Image Signing and Cosign Bypass
  8. Registry Hardening

Registry Security Landscape

Container registries distribute the software that runs in production. Each registry type introduces specific security concerns:

RegistryPrimary RiskTesting Focus
Docker HubPublic image exposure, namespace squattingVisibility settings, access tokens, rate limiting abuse
AWS ECRIAM policy misconfiguration, public ECR exposureRepository policies, cross-account access, lifecycle rules
GCP Artifact RegistryIAM binding misconfig, allUsers exposureIAM audit, vulnerability scanning settings
Azure Container RegistryAdmin account enabled, anonymous pullAdmin credentials, network rules, geo-replication
HarborSelf-hosted misconfig, weak authAPI security, project visibility, replication rules
GitHub GHCRPublic package visibility, package permissionsVisibility settings, package access controls

Image Layer Secret Extraction

Docker images are built in layers. Secrets added in early Dockerfile layers and "removed" in later layers remain in the layer history and are trivially extractable.

Layer Analysis with dive

# Install dive for interactive layer analysis
brew install dive

# Analyze image layers interactively
dive company/app:latest

# Automated layer analysis for secrets
docker save company/app:latest | tar -xO --wildcards "*/layer.tar" | \
  tar -tvf - | grep -iE "\.env|credentials|secrets|config\.json"

# Extract a specific layer for analysis
docker save company/app:latest -o image.tar
tar -xf image.tar
# Each layer is a separate tar file
for layer in */layer.tar; do
  echo "=== Layer: $layer ==="
  tar -tf "$layer" | grep -iE "\.env|secret|credential|password|\.key|\.pem"
done

Secret Extraction from Image History

# Check docker history for secrets in RUN commands
docker history --no-trunc company/app:latest | grep -iE "password|secret|api_key|token"

# Common vulnerable Dockerfile patterns:
# RUN export AWS_ACCESS_KEY=AKIA... && make build
# RUN echo "password=secret123" > /app/config.ini
# ARG API_KEY=sk-prod-abc123  ← ARGs appear in history!
# COPY .env /app/             ← .env file baked into image

# Extract secrets from image metadata
docker inspect company/app:latest | jq '.[0].Config.Env[]' | grep -iE "secret|key|password|token"
docker inspect company/app:latest | jq '.[0].ContainerConfig.Cmd'

# Using Trivy to scan for secrets in images
trivy image --scanners secret company/app:latest

# Using truffleHog to scan image layers
docker save company/app:latest | \
  docker run --rm -i trufflesecurity/trufflehog:latest \
  docker-archive - --only-verified

Multi-Stage Build Analysis

# Multi-stage builds can expose build-stage secrets if COPY --from is careless
# Even if the final image doesn't contain secrets, build image might

# Check for build-stage images in local daemon
docker images | grep -E "^"  # Dangling images from multi-stage builds

# Analyze the build cache
docker system df -v | grep "Build Cache"

# Verify secrets don't leak across stages
# Vulnerable pattern:
FROM golang:1.21 AS builder
ARG GITHUB_TOKEN           # Token visible in layer history
RUN GITHUB_TOKEN=$GITHUB_TOKEN go get ./...

FROM alpine:3.19
COPY --from=builder /app/binary /app/binary
# The builder stage still exists in the build cache with the token

Registry API Exploitation

Docker registries expose a standardized HTTP API (OCI Distribution Spec). Misconfigured authentication allows unauthenticated enumeration and image pulls.

Registry API Enumeration

# Test registry authentication (unauthenticated access check)
curl -s https://registry.company.com/v2/ -I
# 200 OK: no auth required — critical misconfiguration
# 401 with WWW-Authenticate: auth configured

# Enumerate repositories (if unauthenticated access allowed)
curl -s https://registry.company.com/v2/_catalog
curl -s https://registry.company.com/v2/_catalog?n=100  # Paginate

# List tags for a repository
curl -s https://registry.company.com/v2/myapp/tags/list

# Pull image manifest (inspect without downloading full image)
curl -s https://registry.company.com/v2/myapp/manifests/latest \
  -H "Accept: application/vnd.docker.distribution.manifest.v2+json"

# Get image config (contains ENV variables and CMD)
DIGEST=$(curl -s https://registry.company.com/v2/myapp/manifests/latest \
  -H "Accept: application/vnd.docker.distribution.manifest.v2+json" | \
  jq -r '.config.digest')
curl -s https://registry.company.com/v2/myapp/blobs/$DIGEST | jq '.config.Env'

Credential Brute Force and Token Abuse

# Registry basic auth brute force
# Note: use only against systems you own or have authorization to test
hydra -L users.txt -P passwords.txt registry.company.com https-get /v2/

# Docker Hub PAT token scope testing
# Docker Hub tokens can have different scopes: read, write, delete
docker login -u username --password-stdin <<< "dckr_pat_TOKEN"
docker pull company/private-image:latest  # Test read access
docker push company/private-image:test    # Test write access

# ECR authentication token (12-hour validity)
aws ecr get-login-password --region eu-west-1 | \
  docker login --username AWS --password-stdin \
  ACCOUNT.dkr.ecr.eu-west-1.amazonaws.com

# Check token permissions
aws ecr describe-repositories 2>&1 | grep -E "AccessDeniedException|repositories"

Cloud Registry Misconfigurations (ECR/GCR/ACR)

AWS ECR Security Testing

# Check for public ECR repositories (ECR Public Gallery)
aws ecr-public describe-repositories --region us-east-1 2>/dev/null

# Private ECR — check repository policies for cross-account or public access
aws ecr describe-repositories --query 'repositories[].repositoryName' --output text | \
  while read repo; do
    policy=$(aws ecr get-repository-policy --repository-name "$repo" 2>/dev/null)
    if echo "$policy" | grep -q '"AWS":"\*"'; then
      echo "CRITICAL: Public access on $repo"
    fi
    if echo "$policy" | grep -q '"Principal":"\*"'; then
      echo "CRITICAL: Wildcard principal on $repo"
    fi
  done

# Check ECR image scanning configuration
aws ecr describe-repositories \
  --query 'repositories[?imageScanningConfiguration.scanOnPush==`false`].repositoryName'

# Check image vulnerability findings
aws ecr describe-image-scan-findings \
  --repository-name myapp \
  --image-id imageTag=latest \
  --query 'imageScanFindings.findings[?severity==`CRITICAL`]'

# Check lifecycle policies (missing policy = images accumulate, old CVEs persist)
aws ecr get-lifecycle-policy --repository-name myapp 2>/dev/null || echo "No lifecycle policy"

GCP Artifact Registry Security

# Check for publicly accessible repositories
gcloud artifacts repositories list --format="table(name,createTime)"

# Check IAM bindings for public access
gcloud artifacts repositories get-iam-policy REPO_NAME \
  --location=europe-west1 \
  --project=PROJECT_ID | grep -E "allUsers|allAuthenticatedUsers"

# List all repositories and check for overly broad bindings
for repo in $(gcloud artifacts repositories list --format="value(name)"); do
  echo "=== $repo ==="
  gcloud artifacts repositories get-iam-policy $repo 2>/dev/null | grep "members"
done

# Check Container Analysis vulnerability scanning
gcloud container images list-tags gcr.io/PROJECT/IMAGE --show-occurrences \
  --occurrence-filter 'kind="VULNERABILITY" AND (vulnerability.severity="CRITICAL" OR vulnerability.severity="HIGH")'

Azure Container Registry

# Check if admin account is enabled (should be disabled)
az acr show --name myregistry --query "adminUserEnabled"

# If admin is enabled, get credentials
az acr credential show --name myregistry
# admin username + 2 passwords — equivalent to root access to registry

# Check anonymous pull access
az acr show --name myregistry --query "anonymousPullEnabled"

# Check network rules — should restrict to known IPs
az acr show --name myregistry --query "networkRuleSet"

# List repositories
az acr repository list --name myregistry

# Check for public endpoint exposure
az acr show --name myregistry --query "publicNetworkAccess"

Supply Chain Attack Vectors

Image Substitution Attack (Tag Mutability)

Mutable tags allow attackers who gain push access to silently replace production images:

# Check ECR tag immutability
aws ecr describe-repositories \
  --query 'repositories[?imageTagMutability==`MUTABLE`].repositoryName'

# Demonstrate tag mutable attack impact:
# 1. Push malicious image with same tag as production
docker tag attacker/malicious:latest company/app:latest
docker push company/app:latest  # Silently replaces production image!
# 2. Next deploy pulls the malicious image

# Defense: enable immutable tags
aws ecr put-image-tag-mutability \
  --repository-name myapp \
  --image-tag-mutability IMMUTABLE

Namespace Squatting

# Docker Hub namespace squatting:
# If internal images are pulled as "myapp/service" instead of
# "myregistry.company.com/service", they resolve to Docker Hub

# Test for namespace conflicts on Docker Hub
curl -s https://hub.docker.com/v2/repositories/company-internal/ | jq '.count'

# Check pull policy in Kubernetes (must specify full registry path)
kubectl get pods -A -o jsonpath='{range .items[*]}{.spec.containers[*].image}{"\n"}{end}' | \
  grep -v "\.dkr\.ecr\.\|gcr\.io\|azurecr\.io\|ghcr\.io" | \
  grep -v "^$" | sort -u

Dependency Confusion via Base Images

# Check base image sources in Dockerfiles
find . -name "Dockerfile*" -exec grep "^FROM" {} +

# Dangerous: using unpinned or unverified base images
# FROM python:3.11          → resolves to Docker Hub python namespace
# FROM ubuntu:22.04         → Docker Hub official image (generally safe)
# FROM company-base:latest  → typosquatting risk if not from private registry

# Verify base image digests are pinned
grep "^FROM" Dockerfile | grep -v "@sha256:"  # Unpinned base images
# Safe: FROM python:3.11@sha256:abc123...  (immutable digest pin)

Vulnerability Scanning with Trivy and Grype

# Trivy — comprehensive image scanning
# OS packages, language dependencies, misconfigurations, secrets

# Scan a local image
trivy image company/app:latest

# Scan registry image (with auth)
trivy image --username $REG_USER --password $REG_PASS registry.company.com/app:latest

# Generate SARIF for GitHub integration
trivy image --format sarif --output trivy-results.sarif company/app:latest

# Scan Dockerfile for misconfigurations
trivy config Dockerfile

# Fail build on critical CVEs (for CI/CD integration)
trivy image --exit-code 1 --severity CRITICAL company/app:latest

# Grype — alternative vulnerability scanner with broader database
grype company/app:latest
grype company/app:latest --fail-on critical

# Compare scanner results (different databases catch different CVEs)
trivy image company/app:latest --format json > trivy.json
grype company/app:latest -o json > grype.json
# Compare: jq '.Results[].Vulnerabilities[].VulnerabilityID' trivy.json | sort > trivy_cves.txt
# jq '.matches[].vulnerability.id' grype.json | sort > grype_cves.txt
# diff trivy_cves.txt grype_cves.txt

SBOM Generation and Analysis

# Generate Software Bill of Materials from image
trivy image --format spdx-json --output sbom.spdx.json company/app:latest

# Syft for SBOM generation
syft company/app:latest -o spdx-json > sbom.spdx.json
syft company/app:latest -o cyclonedx-json > sbom.cyclonedx.json

# Scan SBOM against vulnerability database
grype sbom:sbom.spdx.json

# Check for specific known-vulnerable packages
syft company/app:latest -o json | \
  jq '.artifacts[] | select(.name == "log4j-core") | {name, version}'

Image Signing and Cosign Bypass

Image signing with cosign verifies image integrity — but policies enforcing signatures can be bypassed if misconfigured.

Testing Image Signing

# Verify cosign signature
cosign verify company/app:latest \
  --certificate-identity=signer@company.com \
  --certificate-oidc-issuer=https://accounts.google.com

# Check if Kubernetes admission controller enforces signatures
kubectl get constrainttemplate -A 2>/dev/null | grep -i "cosign\|sign\|verify"
kubectl get clusterpolicy -A 2>/dev/null  # Kyverno policies
kubectl get imagecontentpolicies -A 2>/dev/null  # OpenShift policies

# Test bypass: submit unsigned image to cluster
kubectl run test-unsigned --image=registry.company.com/unsigned:latest
# If pod starts, signing enforcement is not active

Policy Engine Bypass

# OPA/Gatekeeper bypass via namespace exceptions
# Check for namespaces excluded from image signing policies
kubectl get namespace -o json | \
  jq '.items[] | select(.metadata.labels | has("admission.gatekeeper.sh/ignore")) | .metadata.name'

# Kyverno policy bypass via resource annotations
# Some policies skip resources with specific annotations
kubectl run bypass --image=unsigned:latest \
  --annotations='kyverno.io/ignore=true'  # May bypass policy depending on config

# Check Kyverno policy enforcement mode
kubectl get clusterpolicy -o yaml | grep "validationFailureAction"
# audit: logs violations but allows — NOT enforcing
# enforce: blocks violations — actually secure

Registry Hardening

Dockerfile Security Best Practices

# Secure Dockerfile pattern
FROM python:3.11.9-slim@sha256:abc123...  # Pinned digest, not tag

# Don't use ARG for secrets — they appear in build history
# BAD:  ARG API_KEY
# GOOD: Pass secrets via Docker BuildKit secrets (not in history)
RUN --mount=type=secret,id=api_key \
    API_KEY=$(cat /run/secrets/api_key) pip install --index-url ...

# Use multi-stage builds to minimize final image surface
FROM golang:1.22 AS builder
COPY . .
RUN CGO_ENABLED=0 go build -o /app/server .

FROM scratch  # Minimal base — no shell, no package manager
COPY --from=builder /app/server /server
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
USER 65534  # Non-root
ENTRYPOINT ["/server"]

Registry Access Control

# ECR — enforce immutable tags and encryption
resource "aws_ecr_repository" "app" {
  name                 = "myapp"
  image_tag_mutability = "IMMUTABLE"

  encryption_configuration {
    encryption_type = "KMS"
    kms_key = aws_kms_key.ecr.arn
  }

  image_scanning_configuration {
    scan_on_push = true
  }
}

# Restrict ECR access to specific IAM roles only
resource "aws_ecr_repository_policy" "app" {
  repository = aws_ecr_repository.app.name
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = {"AWS": "arn:aws:iam::ACCOUNT:role/EKSNodeRole"}
      Action    = ["ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage"]
    }]
  })
}
Registry Security Checklist: Enable tag immutability; scan images on push with vulnerability scanner; pin base images to digests (not tags); remove baked-in secrets with BuildKit mount secrets; restrict registry access to specific IAM roles/service accounts; enforce image signing for production workloads; review cross-account and public access policies quarterly.
Security TestToolPriority
Image layer secret extractiontrivy, dive, docker historyCritical
Registry unauthenticated accesscurl registry APICritical
ECR public repository checkAWS CLICritical
Tag mutability auditAWS/GCP/Azure CLIHigh
Image vulnerability scanTrivy, GrypeHigh
Admin credentials check (ACR)Azure CLIHigh
Namespace squatting testDocker Hub APIHigh
Image signing enforcementcosign, kubectlMedium
SBOM generation and analysisSyft, GrypeMedium
Dockerfile misconfiguration scantrivy config, hadolintMedium

Automate Container Registry Security Testing

Ironimo scans container images for vulnerabilities, checks registry access policies, detects secrets baked into layers, and monitors for supply chain risks — all automatically.

Start free scan