Docker Hub Hardening Guide
Container registry security for access tokens, image signing, and repository controls
Overview
Docker Hub is the largest public container registry with millions of images. Research in 2024 found 10,456 images exposing secrets including 4,000 AI model API keys. The 2019 breach affected 190,000 accounts, and OAuth tokens for autobuilds remain perpetual attack vectors. TeamTNT attacks (2021-2022) used compromised accounts to distribute cryptomining malware with 150,000+ malicious image pulls.
Intended Audience
- Security engineers managing container security
- DevOps engineers configuring container registries
- GRC professionals assessing container supply chain
- Platform teams managing Docker infrastructure
How to Use This Guide
- L1 (Baseline): Essential controls for all organizations
- L2 (Hardened): Enhanced controls for security-sensitive environments
- L3 (Maximum Security): Strictest controls (use private registry)
Scope
This guide covers Docker Hub security configurations including authentication, access controls, and integration security.
Table of Contents
1. Authentication & Access Controls
1.1 Enforce MFA and SSO
Profile Level: L1 (Baseline) NIST 800-53: IA-2(1)
Description
Require MFA for Docker Hub accounts, especially those with push access.
Rationale
Why This Matters:
- 2019 breach affected 190,000 accounts
- Compromised accounts distribute malicious images
- TeamTNT used compromised accounts for cryptomining malware
ClickOps Implementation
Step 1: Enable MFA
- Navigate to: Account Settings → Security
- Enable: Two-Factor Authentication
- Configure TOTP or security key
Step 2: Configure SSO (Business)
- Navigate to: Organization → Settings → Security
- Configure SAML SSO
- Enforce SSO for all members
1.2 Implement Access Tokens
Profile Level: L1 (Baseline) NIST 800-53: IA-5
Description
Use personal access tokens instead of passwords for automation.
ClickOps Implementation
Step 1: Create Scoped Tokens
- Navigate to: Account Settings → Security → Access Tokens
- Create tokens with minimum permissions:
- Read-only: For CI/CD pulls
- Read/Write: For builds (specific repos)
Step 2: Rotate Tokens
| Token Type | Rotation |
|---|---|
| CI/CD pull | Quarterly |
| Build/push | Monthly |
Code Implementation
2. Image Security
2.1 Enable Docker Scout
Profile Level: L1 (Baseline) NIST 800-53: RA-5
Description
Use Docker Scout for vulnerability scanning.
Implementation
Code Pack: CLI Script
# Enable Scout for repository
docker scout recommendations myimage:latest
# Check for vulnerabilities
docker scout cves myimage:latest
2.2 Image Signing (Content Trust)
Profile Level: L2 (Hardened) NIST 800-53: SI-7
Description
Enable Docker Content Trust for image signing.
Important: Docker is officially retiring DCT (Docker Content Trust) for Docker Official Images. For new deployments, use Cosign/Sigstore (Section 2.4) instead. DCT is documented here for existing deployments.
Code Pack: CLI Script
# Enable content trust
export DOCKER_CONTENT_TRUST=1
# Sign and push image
docker push myorg/myimage:latest
2.3 Pin Images by Digest, Not Tag
Profile Level: L1 (Baseline) NIST 800-53: SI-7, SA-12 CIS Controls: 2.5
Description
Reference container images by their immutable SHA256 digest instead of mutable tags. Docker Hub tags (including latest, version tags like 0.69.3, and semver tags like v1) are mutable pointers that can be silently replaced by anyone with push access.
Rationale
Attack Vector: Tag mutation — an attacker with push access force-pushes a malicious image to an existing tag, or creates new tags (e.g., 0.69.5, 0.69.6) that appear to be legitimate version increments.
Real-World Incident:
- trivy Docker Hub compromise (March 2026): After poisoning
trivy-actionGitHub Actions tags, the attacker pushed Docker Hub imagesaquasec/trivy:0.69.5andaquasec/trivy:0.69.6— neither had corresponding GitHub releases. Version0.69.6was tagged aslatest, meaning anydocker pull aquasec/trivywithout a pinned digest received the compromised image containing the TeamPCP Cloud Stealer. The malicious payload read/proc/*/memto harvest cloud credentials and exfiltrated them toscan.aquasecurtiy.org.
Why This Matters: Unlike AWS ECR, Google Artifact Registry, and Azure Container Registry, Docker Hub has no tag immutability feature. Tags are always mutable. Digest pinning is the only defense against tag manipulation on Docker Hub.
ClickOps Implementation
Step 1: Find the Digest of a Trusted Image
- Go to Docker Hub and navigate to the image’s Tags tab
- Click on the specific tag to see its digest (starts with
sha256:) - Or run:
docker manifest inspect <image>:<tag>locally
Step 2: Update References to Use Digests
- In Dockerfiles: Change
FROM image:tagtoFROM image@sha256:<digest> - In docker-compose.yml: Change
image: name:tagtoimage: name@sha256:<digest> - In CI/CD workflows: Pin container images in
jobs.*.container.image - In Kubernetes manifests: Pin
spec.containers[].imageto digests
Step 3: Automate Digest Updates
- Use Renovate Bot or Dependabot to automatically propose digest updates when upstream images change
- Configure a weekly schedule for digest update PRs
- Review digest updates before merging — verify they correspond to legitimate releases
Time to Complete: ~15 minutes per repository
Code Implementation
Code Pack: CLI Script
# Get the current digest of an image tag
docker inspect --format='{{index .RepoDigests 0}}' aquasec/trivy:0.69.3
# Output: aquasec/trivy@sha256:abc123...
# Pin by digest in Dockerfile
# VULNERABLE: mutable tag
# FROM aquasec/trivy:latest
# FROM aquasec/trivy:0.69.3
# HARDENED: immutable digest
# FROM aquasec/trivy@sha256:<digest>
# Pin by digest in docker-compose.yml
# services:
# scanner:
# image: aquasec/trivy@sha256:<digest>
# Pin by digest in GitHub Actions
# jobs:
# scan:
# container:
# image: aquasec/trivy@sha256:<digest>
# Audit: find all mutable tag references in Dockerfiles
echo "=== Unpinned Image References ==="
find . -name 'Dockerfile*' -o -name 'docker-compose*.yml' | while read -r file; do
grep -nE 'FROM\s+\S+:\S+' "$file" | grep -vE '@sha256:' | while read -r line; do
echo " $file:$line"
done
grep -nE 'image:\s+\S+:\S+' "$file" | grep -vE '@sha256:' | while read -r line; do
echo " $file:$line"
done
done
# Verify a digest matches expected value
IMAGE="aquasec/trivy:0.69.3"
EXPECTED_DIGEST="sha256:<known-good-digest>"
ACTUAL_DIGEST=$(docker manifest inspect "$IMAGE" 2>/dev/null | \
python3 -c "import json,sys; print(json.load(sys.stdin).get('config',{}).get('digest',''))" 2>/dev/null)
if [ "$ACTUAL_DIGEST" = "$EXPECTED_DIGEST" ]; then
echo "OK: $IMAGE digest matches"
else
echo "ALERT: $IMAGE digest mismatch!"
echo " Expected: $EXPECTED_DIGEST"
echo " Actual: $ACTUAL_DIGEST"
fi
Validation & Testing
- All Dockerfiles use
@sha256:references (no mutable tags) - docker-compose files use digest-pinned images
- CI/CD workflows pin container images by digest
- Renovate or Dependabot configured for automated digest updates
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC8.1 | Change management |
| NIST 800-53 | SI-7, SA-12 | Software integrity, supply chain protection |
| SLSA | Build L2 | Pinned dependencies |
| CIS Controls | 2.5 | Allowlist authorized software |
2.4 Verify Images with Cosign/Sigstore
Profile Level: L2 (Hardened) NIST 800-53: SI-7, SA-12
Description
Use Sigstore Cosign for keyless image signing and verification. Cosign is the recommended replacement for Docker Content Trust (DCT), providing OIDC-based identity binding, transparency logging via Rekor, and OCI artifact storage for signatures.
Rationale
Why Cosign Over DCT:
- DCT is being retired by Docker for Official Images
- Cosign supports keyless signing (no key management burden)
- Signatures are tied to OIDC identity (specific CI/CD workflow, not just an account)
- Transparency log (Rekor) provides public auditability
- Works across all OCI registries, not just Docker Hub
Attack Prevention: In the Trivy Docker Hub compromise, Cosign verification with identity pinning would have detected the malicious images immediately — the attacker’s push would not have a valid signature from the legitimate Aqua Security CI/CD pipeline.
ClickOps Implementation
Step 1: Install Cosign
- macOS:
brew install cosign - Linux: Download from GitHub releases
- CI: Use
sigstore/cosign-installerGitHub Action
Step 2: Sign Images in CI/CD
- Add
id-token: writepermission to your workflow - Install cosign via
sigstore/cosign-installer@v3 - After
docker push, runcosign sign <image>@<digest> - Keyless signing automatically uses the workflow’s OIDC identity
Step 3: Verify Before Deployment
- Add a verification step before any
docker pullor deployment - Pin the expected signer identity and OIDC issuer
- Fail the pipeline if verification fails
Time to Complete: ~30 minutes for CI/CD integration
Code Implementation
Code Pack: CLI Script
# Install cosign
# brew install cosign (macOS)
# go install github.com/sigstore/cosign/v2/cmd/cosign@latest (Go)
# Sign an image (keyless, uses OIDC identity from CI)
cosign sign myorg/myimage@sha256:<digest>
# Sign with a key pair (for air-gapped environments)
cosign generate-key-pair
cosign sign --key cosign.key myorg/myimage@sha256:<digest>
# Verify an image signature with identity pinning
cosign verify myorg/myimage@sha256:<digest> \
--certificate-identity-regexp='.*@myorg\.com' \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com
# Verify in CI/CD before deployment
cosign verify aquasec/trivy@sha256:<digest> \
--certificate-identity-regexp='.*aquasecurity.*' \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com \
|| { echo "ALERT: Image signature verification failed!"; exit 1; }
# GitHub Actions workflow for build + sign + verify
# name: Build and Sign
# on: push
# permissions:
# id-token: write # Required for keyless signing
# packages: write
# jobs:
# build:
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# - uses: sigstore/cosign-installer@v3
# - run: |
# docker build -t ghcr.io/${{ github.repository }}:${{ github.sha }} .
# docker push ghcr.io/${{ github.repository }}:${{ github.sha }}
# cosign sign ghcr.io/${{ github.repository }}@$(docker inspect --format='{{index .RepoDigests 0}}' ghcr.io/${{ github.repository }}:${{ github.sha }} | cut -d@ -f2)
Validation & Testing
- Build pipeline signs images with Cosign after push
- Deployment pipeline verifies signatures before pull
- Signature identity is pinned to expected OIDC issuer and subject
- Unsigned images are rejected by deployment policies
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC8.1 | Change management |
| NIST 800-53 | SI-7, SA-12 | Software integrity, supply chain protection |
| SLSA | Build L3 | Non-falsifiable provenance |
| CIS Controls | 2.6 | Allowlist authorized libraries |
2.5 Generate Build Provenance and SBOM Attestations
Profile Level: L2 (Hardened) NIST 800-53: SA-12, SI-7
Description
Generate SLSA provenance attestations and Software Bill of Materials (SBOM) for container images during build. Provenance proves where, when, and how an image was built. SBOM enumerates all components inside the image.
Rationale
Attack Detection: In the Trivy Docker Hub compromise, the “ghost” images (0.69.5, 0.69.6) had no build provenance — they were pushed directly, not built by CI/CD. Provenance verification would have immediately flagged them as suspicious since they lacked attestations from Aqua Security’s build pipeline.
Why This Matters:
- Provenance attestations prove the image was built by a trusted CI system from a specific source commit
- SBOM attestations enable consumers to check for vulnerable components without pulling the full image
- Docker BuildKit generates provenance by default (minimum mode) since BuildKit 0.11
ClickOps Implementation
Step 1: Enable Provenance in Builds
- Use
docker buildx buildwith--provenance=mode=maxfor full provenance - Add
--sbom=trueto generate SBOM attestations - Push to registry — attestations are stored as OCI artifacts alongside the image
Step 2: Inspect Attestations
- Run
docker buildx imagetools inspect <image>to view provenance - Use
cosign verify-attestationfor cryptographic verification
Time to Complete: ~15 minutes
Code Implementation
Code Pack: CLI Script
# Build with maximum provenance (SLSA Build L2+)
docker buildx build \
--provenance=mode=max \
--sbom=true \
-t myorg/myimage:v1 \
--push .
# Inspect provenance of an image
docker buildx imagetools inspect myorg/myimage:v1 \
--format '{{ json .Provenance }}'
# Inspect SBOM of an image
docker buildx imagetools inspect myorg/myimage:v1 \
--format '{{ json .SBOM }}'
# Verify build provenance with cosign
cosign verify-attestation myorg/myimage@sha256:<digest> \
--type slsaprovenance \
--certificate-identity-regexp='.*@myorg\.com' \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com
# GitHub Actions: build with attestations
# - uses: docker/build-push-action@v5
# with:
# push: true
# tags: myorg/myimage:${{ github.sha }}
# provenance: mode=max
# sbom: true
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC8.1 | Change management |
| NIST 800-53 | SA-12, SI-7 | Supply chain protection, software integrity |
| SLSA | Build L2/L3 | Signed provenance |
3. Repository Security
3.1 Private Repository Configuration
Profile Level: L1 (Baseline)
ClickOps Implementation
- Set repositories to Private by default
- Configure team access (not individual)
- Audit repository permissions quarterly
3.2 Prevent Secret Exposure
Profile Level: L1 (Baseline)
Implementation
- Scan images for secrets before push
- Use multi-stage builds
- Never include credentials in Dockerfiles
4. Monitoring & Detection
4.1 Audit Logging
Profile Level: L1 (Baseline) NIST 800-53: AU-2
Detection Focus
Monitor Docker Hub activity logs for:
- Image push events from unexpected accounts or IP addresses
- New tags created outside normal CI/CD schedules
- The
latesttag being moved - Push events without corresponding GitHub release/tag events
4.2 Detect Unauthorized and Ghost Image Pushes
Profile Level: L1 (Baseline) NIST 800-53: SI-4, AU-6 CIS Controls: 8.5
Description
Detect “ghost” image pushes — Docker Hub images that have no corresponding source code release, tag, or build pipeline run. Ghost images indicate either a compromised push credential or a supply chain attack.
Rationale
Attack Vector: Ghost image push — an attacker with push access creates new image tags that have no corresponding GitHub release, source tag, or CI/CD build record. Because the version number increments naturally (e.g., 0.69.5 after 0.69.4), consumers assume the new version is legitimate.
Real-World Incident:
- trivy Docker Hub compromise (March 2026): The attacker pushed
aquasec/trivy:0.69.5andaquasec/trivy:0.69.6to Docker Hub — neither version had a corresponding GitHub release, tag, or source commit. Version0.69.6was tagged aslatest. The attack exploited the assumption that Docker Hub images always correspond to source releases.
Anti-Incident-Response TTPs observed:
- Attacker deleted the original incident disclosure discussion (#10265) to slow community awareness
- 17+ spam bot accounts flooded the replacement discussion within 1 second with generic praise messages to bury legitimate alerts
- Taunting messages (“teampcp owns you”) served as both attribution and disruption
Cross-Channel Propagation: The same poisoned binary cascaded through GitHub Releases, GitHub Actions, Docker Hub, Homebrew, and Helm charts simultaneously — compromising the source artifact once and letting distribution automation amplify the attack.
ClickOps Implementation
Step 1: Establish Source-to-Image Mapping
- Document which CI/CD pipeline builds and pushes each Docker Hub image
- Ensure every image push is triggered by a GitHub release or tag event
- Verify that the
latesttag is only moved by your CI/CD pipeline, never manually
Step 2: Set Up Monitoring
- Compare Docker Hub tags against GitHub releases on a schedule (daily minimum)
- Alert on any Docker Hub tag that has no corresponding GitHub release
- Alert when the
latesttag digest changes outside of a CI/CD run - Monitor for unexpected version increments (e.g.,
0.69.5when the latest release is0.69.4)
Step 3: Respond to Ghost Images
- If a ghost image is detected, immediately check if push credentials are compromised
- Rotate all Docker Hub access tokens
- Remove the ghost image tags
- Notify consumers that the tags may have been malicious
- Check Homebrew, Helm charts, and other downstream distributors for automatic propagation
Time to Complete: ~20 minutes for initial setup; ongoing monitoring
Code Implementation
Code Pack: CLI Script
# Usage: ./detect-ghost-pushes.sh <namespace/repo> <github-org/repo>
DOCKER_REPO="${1:-aquasec/trivy}"
GITHUB_REPO="${2:-aquasecurity/trivy}"
echo "=== Ghost Image Push Detection ==="
echo "Docker Hub: $DOCKER_REPO"
echo "GitHub: $GITHUB_REPO"
echo ""
# Step 1: List Docker Hub tags
echo "--- Docker Hub Tags (last 20) ---"
curl -s "https://hub.docker.com/v2/repositories/$DOCKER_REPO/tags?page_size=20&ordering=-last_updated" | \
python3 -c "
import json, sys
data = json.load(sys.stdin)
for tag in data.get('results', []):
name = tag['name']
updated = tag.get('last_updated', 'unknown')
digest = tag.get('digest', 'unknown')[:19]
print(f' {name:20s} {updated[:19]:20s} {digest}')
"
# Step 2: List GitHub releases
echo ""
echo "--- GitHub Releases (last 20) ---"
gh api "repos/$GITHUB_REPO/releases?per_page=20" --jq '.[].tag_name' 2>/dev/null | \
while read -r tag; do echo " $tag"; done
# Step 3: Find Docker tags with no matching GitHub release
echo ""
echo "--- Tags on Docker Hub with NO GitHub Release ---"
DOCKER_TAGS=$(curl -s "https://hub.docker.com/v2/repositories/$DOCKER_REPO/tags?page_size=100" | \
python3 -c "import json,sys; [print(t['name']) for t in json.load(sys.stdin).get('results',[])]" 2>/dev/null)
GITHUB_TAGS=$(gh api "repos/$GITHUB_REPO/tags?per_page=100" --jq '.[].name' 2>/dev/null | sed 's/^v//')
for dtag in $DOCKER_TAGS; do
# Skip non-version tags
echo "$dtag" | grep -qE '^[0-9]+\.[0-9]+' || continue
if ! echo "$GITHUB_TAGS" | grep -qx "$dtag"; then
echo " ALERT: $DOCKER_REPO:$dtag has NO matching GitHub release"
fi
done
# Step 4: Check if 'latest' tag matches the latest GitHub release
echo ""
echo "--- Latest Tag Verification ---"
LATEST_GH=$(gh api "repos/$GITHUB_REPO/releases/latest" --jq '.tag_name' 2>/dev/null | sed 's/^v//')
echo " Latest GitHub release: $LATEST_GH"
echo " Verify: docker pull $DOCKER_REPO:$LATEST_GH and compare digest to :latest"
Validation & Testing
- Script detects Docker Hub tags with no matching GitHub release
- Monitoring alerts configured for unexpected
latesttag changes - Push credential rotation procedure documented and tested
- Downstream distribution channels (Homebrew, Helm) included in response plan
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC7.2, CC7.3 | Detection and monitoring, incident response |
| NIST 800-53 | SI-4, AU-6 | System monitoring, audit review |
| CIS Controls | 8.5 | Collect detailed audit logs |
Appendix A: Recommendation for High-Security
For high-security environments, consider:
- Private container registry (Harbor, ECR, GCR, ACR)
- Air-gapped registry for production
- Image signing with Sigstore/Cosign
- Supply chain attestations (SLSA)
Appendix B: References
Official Docker Documentation:
- Docker Trust Center
- Docker Security
- Docker Compliance
- Docker Hub Documentation
- Docker Engine Security
- Security Announcements
API & Developer Documentation:
Compliance Frameworks:
- SOC 2 Type II, ISO 27001 — via Docker Compliance
- Annual penetration testing of Docker Hub, Desktop, Scout, and Build Cloud
- GDPR and CCPA compliant
Security Incidents:
- 2019 Docker Hub Breach: Unauthorized access exposed usernames, hashed passwords, and GitHub/Bitbucket tokens for approximately 190,000 accounts.
- 2024 Secret Exposure Research: Flare discovered 10,456 Docker Hub images exposing secrets including API keys, cloud credentials, and CI/CD tokens.
- 2025 Desktop Vulnerabilities: CVE-2025-13743 (expired Hub PATs in diagnostics logs) and CVE-2025-9164 (Windows installer DLL hijacking for local privilege escalation).
- TeamTNT Campaigns (2021-2022): Compromised Docker Hub accounts used to distribute cryptomining malware with 150,000+ malicious image pulls.
- Trivy Docker Hub Compromise (March 2026): TeamPCP pushed
aquasec/trivy:0.69.5and0.69.6to Docker Hub with no corresponding GitHub releases. Version0.69.6was tagged aslatest. The images contained a three-stage credential stealer that read/proc/*/memand exfiltrated cloud credentials toscan.aquasecurtiy.org. This was part of a broader supply chain attack that also poisoned 75trivy-actionGitHub Actions tags, Homebrew packages, and Helm charts. See Sections 2.3, 2.4, 2.5, and 4.2 for hardening controls.
Changelog
| Date | Version | Maturity | Changes | Author |
|---|---|---|---|---|
| 2026-03-23 | 0.2.0 | draft | Add digest pinning, Cosign verification, build provenance, ghost image detection controls (Trivy Docker Hub supply chain attack) | Claude Code (Opus 4.6) |
| 2025-12-14 | 0.1.0 | draft | Initial Docker Hub hardening guide | Claude Code (Opus 4.5) |