Docker » Registries & Supply-Chain Security
A registry is where images live between docker build and docker run. Everything downstream — what runs in production, what a vulnerability scanner reports, what an auditor trusts — depends on the integrity of that distribution step. This page covers how registries store and address images, how tags differ from immutable digests, and the modern supply-chain controls (signing, SBOMs, scanning, and provenance) that let you prove an image is the one you built from source you control.
How a Registry Works
A registry is an HTTP service that implements the OCI Distribution Specification (the open successor to the Docker Registry HTTP API v2). When you docker push, the client uploads two kinds of objects:
- Blobs — the compressed layer tarballs and the image config JSON. Each is content-addressed by the SHA-256 digest of its bytes.
- Manifests — small JSON documents that list the config blob and the ordered layer blobs (by digest and size) that make up an image. Manifests are themselves content-addressed.
A tag is just a mutable human-friendly name (a pointer) stored in the registry that resolves to a manifest digest. A multi-arch image uses a manifest list (also called an image index): one top-level manifest whose entries are per-platform manifests (linux/amd64, linux/arm64, …). The client picks the entry matching its platform.
Pushing and pulling both reduce to “exchange blobs and manifests by digest.” Because every object is named by the hash of its content, the registry can deduplicate shared layers across images and the client can verify integrity end-to-end.
The digest you see in docker images --digests or docker buildx imagetools inspect is the SHA-256 of the manifest bytes. That single hash transitively pins the config and every layer, because the manifest references them by their own digests. This is the property that makes digests trustworthy: change anything inside the image and the top-level digest changes.
Image References Anatomy
ghcr.io/acme/web-api:1.4.2@sha256:9f86d08...
└──┬──┘ └───┬────┘ └─┬─┘ └────────┬────────┘
host repository tag digest
| Part | Example | Notes |
|---|---|---|
| Host (registry) | ghcr.io |
Omitted defaults to Docker Hub (registry-1.docker.io) |
| Repository (namespace + name) | acme/web-api |
On Docker Hub, a bare name like nginx means library/nginx |
| Tag | 1.4.2 |
Mutable pointer; defaults to latest if omitted |
| Digest | sha256:9f86d08... |
Immutable; when present, overrides the tag |
When both a tag and a digest are present, the digest wins — the tag is informational. Pulling image:tag@sha256:... fetches exactly that manifest and errors if the tag no longer points there.
Choosing a Registry
Most teams use one of a few registries. They all speak the OCI distribution protocol, so docker, buildx, crane, and cosign work against any of them; they differ in hosting model, authentication, and integration.
| Registry | Hosting | Auth model | Best for |
|---|---|---|---|
| Docker Hub | SaaS (Docker, Inc.) | Docker ID / PAT | Public images, official base images, getting started |
| GitHub Container Registry (GHCR) | SaaS (GitHub) | GitHub token / PAT | Repos already on GitHub; tight Actions integration |
| Amazon ECR | SaaS (AWS) | IAM (short-lived tokens) | Workloads on ECS/EKS; IAM-native access control |
| Google Artifact Registry (GAR/GCR) | SaaS (Google Cloud) | IAM / service accounts | GKE and Cloud Run deployments |
| Harbor | Self-hosted (CNCF) | Local / OIDC / LDAP | On-prem, air-gapped, policy-driven enterprises |
| Azure Container Registry (ACR) | SaaS (Azure) | Entra ID / tokens | AKS and Azure-native pipelines |
Authenticating and Pushing
The mechanics are identical across providers — only the login step differs because each has its own credential source.
# Docker Hub (default registry; tag with your namespace)
docker login -u myuser
docker tag myapp:1.4.2 myuser/myapp:1.4.2
docker push myuser/myapp:1.4.2
# GitHub Container Registry (use a PAT or the Actions GITHUB_TOKEN)
echo "$CR_PAT" | docker login ghcr.io -u my-gh-user --password-stdin
docker tag myapp:1.4.2 ghcr.io/acme/myapp:1.4.2
docker push ghcr.io/acme/myapp:1.4.2
# Amazon ECR (short-lived token from the AWS CLI, repo must exist)
aws ecr get-login-password --region us-east-1 \
| docker login --password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.com
docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:1.4.2
# Google Artifact Registry (helper wires gcloud creds into the Docker client)
gcloud auth configure-docker us-docker.pkg.dev
docker push us-docker.pkg.dev/my-project/my-repo/myapp:1.4.2
ECR tokens expire after 12 hours, which is why CI authenticates on every run rather than storing a static password. GHCR inside GitHub Actions can authenticate with the automatically provisioned GITHUB_TOKEN, scoped to the repository — no long-lived secret required.
A few axes distinguish them:
- Self-host vs SaaS — Harbor gives you on-prem control, replication, and air-gap support; managed registries remove the ops burden.
- Identity-native auth — cloud registries fold into IAM, so access follows your existing roles instead of separate registry credentials.
- Replication and caching — Harbor and pull-through caches mirror upstream images to cut egress and survive upstream outages.
- Retention and GC — tag retention rules plus garbage collection reclaim space from untagged, content-addressed blobs.
Harbor: a Self-Hosted Registry
Harbor is the CNCF-graduated open-source registry. Beyond storing images it adds project-level RBAC, built-in Trivy scanning, image signing enforcement, replication between Harbor instances (and to/from Docker Hub or ECR), and a pull-through proxy cache. It is the common choice for air-gapped or compliance-driven environments where images cannot leave the perimeter. You point the Docker client at it exactly like any other registry:
docker login harbor.internal.acme.com
docker push harbor.internal.acme.com/platform/myapp:1.4.2
Tagging Strategy and Immutability
Tags are mutable by default, and that mutability is the single largest source of “works in CI, breaks in prod” incidents. Two builds can both be tagged latest; whichever pushed last wins, and a pull tomorrow may silently get a different image than a pull today.
Why latest Is a Trap
latest is not special to the registry — it is just the tag applied when you omit one. It carries no guarantee of being the newest or most stable build. Deploying myapp:latest means your running version depends on push timing and pull caching, which is irreproducible. Pin deployments to a specific tag, and verify with a digest.
A Workable Tagging Scheme
Apply multiple tags to the same image so humans and machines each get what they need:
# One build, several tags pointing at the same digest
docker build -t myapp:1.4.2 .
docker tag myapp:1.4.2 myapp:1.4
docker tag myapp:1.4.2 myapp:1
docker tag myapp:1.4.2 myapp:sha-$(git rev-parse --short HEAD)
docker push --all-tags myapp
| Tag style | Example | Mutability | Use |
|---|---|---|---|
| Immutable build ID | sha-a1b2c3d |
Effectively immutable | Deployments, rollbacks, audit trails |
| Exact semver | 1.4.2 |
Should be immutable | Release pinning |
| Rolling minor/major | 1.4, 1 |
Moves forward | “Latest patch of 1.4” convenience |
| Channel | stable, edge |
Moves forward | Human-facing convenience only |
The sha-<commit> tag ties an image back to the exact source commit that produced it — invaluable when correlating a production incident with a code change.
Enforcing Immutability at the Registry
Convention is not enough; enforce it server-side so a tag can never be overwritten once published:
- Amazon ECR — set the repository to
IMMUTABLEtag mutability; re-pushing an existing tag is rejected. - Harbor — define immutability rules per project matching tag patterns (e.g.
v*). - Google Artifact Registry — enable immutable tags on the repository.
- Universal — deploy by digest (
image@sha256:...) so the tag is irrelevant to what actually runs.
# ECR: make a repository tag-immutable
aws ecr create-repository --repository-name myapp \
--image-tag-mutability IMMUTABLE
# Resolve a tag to its digest once, then deploy the digest forever
docker buildx imagetools inspect ghcr.io/acme/myapp:1.4.2 \
--format '{{.Manifest.Digest}}'
# -> sha256:9f86d081884c7d65... (use this in your manifests)
The Software Supply Chain Problem
Pinning a digest proves an image has not changed since you recorded that digest. It does not prove who built it, from what source, or whether it contains known-vulnerable components. Supply-chain security adds those guarantees. The mental model is a chain of attestations, each answering a different question:
| Control | Question it answers | Tool(s) |
|---|---|---|
| Digest pinning | Did the bytes change? | Registry / OCI digests |
| Signing | Did we publish this, with our key? | cosign, Notation, DCT |
| SBOM | What components are inside? | Syft, BuildKit |
| Vulnerability scan | Do those components have known CVEs? | Trivy, Grype, Scout |
| Provenance | How and from what source was it built? | SLSA, BuildKit attestations |
These are independent and composable. A mature pipeline produces all of them as OCI artifacts stored next to the image in the same registry, then enforces them at deploy time with an admission policy.
Image Signing
Signing binds a cryptographic identity to an image digest, so a verifier can reject anything not signed by an expected key or identity. There are three approaches you will encounter.
Docker Content Trust (legacy)
The original mechanism, built on The Update Framework (TUF) via Notary v1. Setting DOCKER_CONTENT_TRUST=1 makes the Docker client sign on push and verify on pull:
export DOCKER_CONTENT_TRUST=1
docker push myorg/myapp:1.4.2 # prompts to create/use signing keys
docker pull myorg/myapp:1.4.2 # fails if the tag is unsigned
DCT works but is tied to the Docker CLI, uses tag-based (not digest-based) trust, and the Notary v1 ecosystem is effectively in maintenance. New designs should prefer cosign or Notation, which store signatures as standard OCI artifacts any tool can read.
cosign (Sigstore)
cosign, part of the Sigstore project, is the de facto standard. It signs the image digest and stores the signature as an OCI artifact alongside the image. It supports two modes:
Key-based — you hold a key pair:
cosign generate-key-pair # cosign.key + cosign.pub
cosign sign --key cosign.key ghcr.io/acme/myapp@sha256:9f86d08...
cosign verify --key cosign.pub ghcr.io/acme/myapp@sha256:9f86d08...
Keyless — the headline feature. Instead of managing long-lived keys, cosign requests a short-lived certificate from the Sigstore Fulcio CA, bound to an OIDC identity (a GitHub Actions workload, a Google account, etc.). The signing event is recorded in the Rekor transparency log. There is no key to leak or rotate:
# In CI, COSIGN_EXPERIMENTAL not needed on modern cosign; uses ambient OIDC
cosign sign ghcr.io/acme/myapp@sha256:9f86d08...
# Verify that the signer was a specific GitHub Actions workflow
cosign verify ghcr.io/acme/myapp@sha256:9f86d08... \
--certificate-identity-regexp 'https://github.com/acme/.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
Keyless verification checks two things: the certificate’s identity (who) and its OIDC issuer (where the identity came from). That pair is far harder to forge than possession of a static key.
Notation (Notary v2 / CNCF)
Notation is the CNCF Notary Project’s signer. Like cosign it produces OCI-native signatures, but it is built around x.509 PKI and trust policies rather than Sigstore’s keyless model — a better fit for enterprises with an existing certificate authority. Azure Container Registry and others integrate it natively.
notation sign $REGISTRY/myapp@sha256:9f86d08...
notation verify $REGISTRY/myapp@sha256:9f86d08... # uses configured trust policy
Choosing a Signer
| Tool | Trust root | Signature storage | Pick it when |
|---|---|---|---|
| cosign | Keys or keyless (Fulcio + OIDC) | OCI artifact | You want keyless CI signing and the broadest ecosystem |
| Notation | x.509 / your own CA | OCI artifact | You already run PKI and want CA-rooted trust policies |
| Docker Content Trust | TUF / Notary v1 | Separate Notary server | Maintaining existing DCT setups only; avoid for new work |
Software Bill of Materials (SBOM)
An SBOM is a machine-readable inventory of everything inside an image: OS packages, language dependencies, versions, and licenses. It is the difference between “we think we use log4j somewhere” and “image sha256:9f86… contains log4j-core 2.14.1 in /app/lib.” When a new CVE lands, you query SBOMs instead of rebuilding and rescanning the world.
Two standard formats dominate: SPDX (Linux Foundation / ISO standard) and CycloneDX (OWASP). Tools emit either.
Generating an SBOM with Syft
Syft (by Anchore) scans an image’s filesystem and catalogs its components:
# Human-readable table
syft ghcr.io/acme/myapp:1.4.2
# CycloneDX JSON (good input for scanners and dependency tooling)
syft ghcr.io/acme/myapp:1.4.2 -o cyclonedx-json=sbom.cdx.json
# SPDX JSON (widely required for compliance / NTIA minimum elements)
syft ghcr.io/acme/myapp:1.4.2 -o spdx-json=sbom.spdx.json
BuildKit can also generate an SBOM at build time and attach it as an attestation, which is more accurate than scanning the finished image because the builder knows exactly what each stage installed:
docker buildx build --sbom=true --provenance=true \
-t ghcr.io/acme/myapp:1.4.2 --push .
Attaching the SBOM to the image (rather than keeping it in a CI artifact bucket) means anyone who can pull the image can also fetch its bill of materials with docker buildx imagetools inspect --format '{{ json .SBOM }}' or cosign download sbom.
Vulnerability Scanning
A scanner cross-references the components in an image (from its SBOM or by introspecting the filesystem) against vulnerability databases such as the NVD, GitHub Advisory Database, and distro security trackers, then reports matching CVEs by severity.
Trivy and Grype
Trivy (Aqua) and Grype (Anchore) are the two leading open-source scanners. Both scan an image directly or an SBOM you already produced.
# Trivy: scan an image, fail CI on High/Critical only
trivy image --severity HIGH,CRITICAL --exit-code 1 ghcr.io/acme/myapp:1.4.2
# Trivy: scan the SBOM you already built (faster, no re-pull)
trivy sbom sbom.cdx.json
# Grype: scan an image and gate on a severity threshold
grype ghcr.io/acme/myapp:1.4.2 --fail-on high
# Grype: scan a Syft SBOM directly
grype sbom:./sbom.spdx.json
docker scout cves (built into Docker Desktop and the CLI) is a convenient third option that also surfaces remediation advice — “upgrade base image from 3.18 to 3.19 to drop 7 CVEs.”
Making Scan Results Actionable
Raw CVE counts are noise without policy. Practical scanning means:
- Gate on severity, not totals. Failing on every
LOWblocks releases for unfixable noise; gate onHIGH/CRITICALwith a fix available. - Distinguish fixable from unfixable. A CVE with no upstream patch cannot be remediated by you today; Trivy’s
--ignore-unfixedfilters these out of the gate. - Suppress with justification, not silence. Use a
.trivyignore(or VEX document) to waive specific CVEs with a reason and an expiry, rather than lowering the global threshold. - Re-scan continuously. An image clean at build time accumulates CVEs as new ones are disclosed. Scan on a schedule, not just at push.
# Only fail on fixable High/Critical issues
trivy image --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 \
ghcr.io/acme/myapp:1.4.2
This is also where minimal base images pay off directly: a distroless or alpine base ships far fewer packages, so there is simply less surface for a scanner to flag.
Provenance and SLSA
Signing proves who published an image; provenance proves how it was built. Build provenance is a signed statement describing the build: which source repository and commit, which builder, which build parameters, and which materials (dependencies) went in. It is the attestation that lets a verifier reject an image that did not come from your trusted, hermetic pipeline — defending against a compromised CI runner or a tampered build.
SLSA
SLSA (“Supply-chain Levels for Software Artifacts”) is a framework that grades build integrity in levels. Roughly:
| SLSA Build Level | Guarantee |
|---|---|
| L0 | No guarantees |
| L1 | Provenance exists and describes how the artifact was built |
| L2 | Provenance is signed and generated by a hosted build service |
| L3 | Build runs on a hardened, isolated platform; provenance is unforgeable by the build itself |
Higher levels make provenance progressively harder to forge — at L3 the build platform generates and signs provenance in a way the build steps themselves cannot tamper with.
Generating Provenance
BuildKit emits in-toto provenance attestations and attaches them to the image:
# mode=max records the full build: source, build args, base images, etc.
docker buildx build \
--provenance=mode=max \
--sbom=true \
-t ghcr.io/acme/myapp:1.4.2 --push .
# Inspect the attached attestations
docker buildx imagetools inspect ghcr.io/acme/myapp:1.4.2 \
--format '{{ json .Provenance }}'
GitHub’s official SLSA provenance action and the built-in artifact attestations produce signed, SLSA-aligned provenance directly from Actions, with the signing identity rooted in the workflow itself.
Putting It Together: a Hardened Pipeline
A complete supply-chain-aware build does six things in sequence, producing artifacts that are stored beside the image and enforced at deploy time. The example below uses GitHub Actions with GHCR, keyless cosign, and BuildKit attestations.
# .github/workflows/release.yml
name: build-and-attest
on:
push:
tags: ["v*"]
permissions:
contents: read
packages: write # push to GHCR
id-token: write # OIDC token for keyless cosign
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# 1. Build with provenance + SBOM attestations attached
- id: build
uses: docker/build-push-action@v6
with:
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}
provenance: mode=max
sbom: true
# 2. Scan the freshly pushed image; fail on fixable High/Critical
- uses: aquasecurity/trivy-action@master
with:
image-ref: ghcr.io/${{ github.repository }}:${{ github.ref_name }}
severity: HIGH,CRITICAL
ignore-unfixed: true
exit-code: "1"
# 3. Keyless-sign the image digest (identity = this workflow)
- uses: sigstore/cosign-installer@v3
- run: cosign sign --yes ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
At deploy time, an admission controller (for example Kyverno or Sigstore’s policy-controller) verifies the signature identity and the presence of provenance before the image is allowed to run:
# What a deploy-time gate effectively checks
cosign verify ghcr.io/acme/myapp@sha256:9f86d08... \
--certificate-identity-regexp 'https://github.com/acme/.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
The result is an unbroken chain: a digest no one can alter, a signature only your pipeline can produce, an SBOM listing every component, a scan gating on real risk, and provenance tying the artifact back to a specific commit and builder.
Key Takeaways
- Deploy by digest. Tags are mutable pointers; the
sha256:digest immutably pins the config and every layer. Resolve tags to digests once and deploy the digest. - Enforce immutability server-side. Set ECR/GAR repos or Harbor projects to reject tag overwrites, and never deploy
latest— convention alone is not enough. - Sign with cosign. Keyless cosign signing binds a workflow identity to an image digest with no long-lived keys to leak. Verify identity and issuer.
- SBOM + scan + provenance. Generate an SBOM (Syft/BuildKit), gate on fixable High/Critical CVEs (Trivy/Grype), and attach SLSA provenance so you can prove what shipped and how.
See Also
- Fundamentals - Images, layers, and the content-addressed model behind digests
- Storage & Security - Runtime hardening, secrets, and the scanning basics expanded here
- Dockerfiles & CI/CD - Minimal base images and build pipelines that produce these artifacts
- Advanced Patterns - Production deployment architectures
- Kubernetes - Admission control and verifying signatures at deploy time
- Cybersecurity - Broader security fundamentals