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.
Container registries distribute the software that runs in production. Each registry type introduces specific security concerns:
| Registry | Primary Risk | Testing Focus |
|---|---|---|
| Docker Hub | Public image exposure, namespace squatting | Visibility settings, access tokens, rate limiting abuse |
| AWS ECR | IAM policy misconfiguration, public ECR exposure | Repository policies, cross-account access, lifecycle rules |
| GCP Artifact Registry | IAM binding misconfig, allUsers exposure | IAM audit, vulnerability scanning settings |
| Azure Container Registry | Admin account enabled, anonymous pull | Admin credentials, network rules, geo-replication |
| Harbor | Self-hosted misconfig, weak auth | API security, project visibility, replication rules |
| GitHub GHCR | Public package visibility, package permissions | Visibility settings, package access controls |
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.
# 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
# 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 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
Docker registries expose a standardized HTTP API (OCI Distribution Spec). Misconfigured authentication allows unauthenticated enumeration and image pulls.
# 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'
# 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"
# 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"
# 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")'
# 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"
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
# 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
# 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)
# 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
# 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 with cosign verifies image integrity — but policies enforcing signatures can be bypassed if misconfigured.
# 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
# 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
# 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"]
# 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"]
}]
})
}
| Security Test | Tool | Priority |
|---|---|---|
| Image layer secret extraction | trivy, dive, docker history | Critical |
| Registry unauthenticated access | curl registry API | Critical |
| ECR public repository check | AWS CLI | Critical |
| Tag mutability audit | AWS/GCP/Azure CLI | High |
| Image vulnerability scan | Trivy, Grype | High |
| Admin credentials check (ACR) | Azure CLI | High |
| Namespace squatting test | Docker Hub API | High |
| Image signing enforcement | cosign, kubectl | Medium |
| SBOM generation and analysis | Syft, Grype | Medium |
| Dockerfile misconfiguration scan | trivy config, hadolint | Medium |
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