Education › DevOps › Stage 2: Build & ship

Artifacts & supply chain

Registries, versioning, dependency pinning, image scanning, and SBOM basics.

Beginner–Intermediate ~30 min read Module 8 of 17

The code your team writes is a small fraction of what you ship. The rest is base images, open-source libraries and their own dependencies, pulled from the internet at build time. Attackers know this, and compromising a dependency or a build system reaches thousands of victims at once. This module covers how artifacts are stored, named and versioned, and the practical controls that let you answer two questions at any time: what exactly is running, and is it known to be vulnerable?

After this module you can
  • Explain what an artifact registry is for and how tags differ from digests
  • Apply semantic versioning and an image tagging scheme that makes every deploy traceable
  • Pin dependencies with lockfiles and digests so that builds are reproducible
  • Scan images and dependencies for known vulnerabilities and triage the results sensibly
  • Generate a software bill of materials and explain what signing and provenance add

Artifacts and registries

An artifact is the immutable output of a build: a container image, a JAR, a Python wheel, an npm package, a Helm chart. A registry (or artifact repository) stores artifacts, controls who may push and pull them, and serves them to deploy targets. It is the hand-off point between CI, which produces artifacts, and CD, which consumes them.

Container registries include Docker Hub, GitHub Container Registry, and one from every cloud provider. A full image reference has four parts:

text
ghcr.io / acme/orders-api : 1.4.2   @sha256:9b2c...e41f
   |            |             |              |
registry    repository       tag           digest

The distinction between the last two is the most important idea in this module. A tag is a human-friendly, mutable label: whoever can push may point 1.4.2, and certainly latest, at a different image tomorrow. A digest is the SHA-256 hash of the image manifest. It is immutable and content-addressed: if a single byte changes, the digest changes. Pulling by digest guarantees you get precisely the image you tested.

bash
docker pull ghcr.io/acme/orders-api:1.4.2
docker inspect --format '{{index .RepoDigests 0}}' ghcr.io/acme/orders-api:1.4.2
# ghcr.io/acme/orders-api@sha256:9b2c...

docker buildx imagetools inspect ghcr.io/acme/orders-api:1.4.2   # manifest and platforms
Tip

Many registries let you mark a repository's tags as immutable, so an existing tag can never be overwritten. Turn it on for release repositories; it removes a whole class of "but it was working yesterday" incidents.

Versioning and tagging

Semantic versioning encodes compatibility in the number MAJOR.MINOR.PATCH. Increment PATCH for backwards-compatible bug fixes, MINOR for backwards-compatible new features, and MAJOR for breaking changes. It is a promise to your consumers about what upgrading will cost them, and it is what makes version ranges in package managers meaningful.

For deployable images, tag every build with the commit SHA, which is unique and traceable, and add a semantic version tag when you cut a release. Deploy by SHA tag or digest. Treat latest as a convenience for humans, never as something a production manifest refers to.

bash
SHA="$(git rev-parse --short HEAD)"
IMAGE=ghcr.io/acme/orders-api

docker build -t "$IMAGE:$SHA" .
docker push "$IMAGE:$SHA"

# on a release, add the version tag to the image that was already built and tested
docker tag "$IMAGE:$SHA" "$IMAGE:1.4.2"
docker push "$IMAGE:1.4.2"

Note that the release step tags an existing image; it does not rebuild. Registries also need housekeeping. Set a retention policy that deletes old untagged and per-commit images while keeping released versions, or storage costs grow without limit.

Pinning for reproducible builds

A build is reproducible when the same commit produces the same artifact next month. Version ranges work against that: a manifest that allows ^4.17.0 installs whatever matching version is newest on build day. A lockfile records the exact version, and usually a hash, of every direct and transitive dependency, which is a dependency of a dependency and makes up most of your tree.

EcosystemLockfileInstall exactly what it says
npmpackage-lock.jsonnpm ci
Python (pip-tools)requirements.txt with hashespip install --require-hashes -r requirements.txt
Gogo.sumgo mod download / go mod verify
RustCargo.lockcargo build --locked

Commit the lockfile, and make CI use the strict install command so that a drifted lockfile fails the build instead of being silently updated. Apply the same thinking to base images: FROM python:3.12-slim moves as patches are released, while a digest pin does not.

dockerfile
# tag for humans, digest for the machine
FROM python:3.12-slim@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef

Pinning has a cost: pinned things do not update themselves, including for security fixes. The answer is not to stop pinning but to automate updates. Tools such as Dependabot and Renovate open pull requests that bump lockfiles and digests, and your pipeline tests each one. You get reproducibility and freshness, with every change reviewed and revertable.

Scanning for known vulnerabilities

A vulnerability scanner lists the packages in an image or lockfile and matches them against databases of published vulnerabilities, identified by CVE numbers. Open-source scanners include Trivy and Grype; registries and Git hosts often have one built in. Run a scan in the pipeline on every build, and re-scan what is already deployed on a schedule, because new CVEs are published against old images every day.

bash
trivy image ghcr.io/acme/orders-api:1.4.2             # OS packages and language deps
trivy image --severity HIGH,CRITICAL --ignore-unfixed \
  --exit-code 1 ghcr.io/acme/orders-api:1.4.2        # fail the pipeline on serious, fixable issues
trivy fs .                                            # scan the repo's lockfiles
trivy config .                                        # misconfigurations in Dockerfiles, IaC

The first scan of a typical image returns a discouraging list. Triage it with three questions.

  1. Is there a fix? If a patched version exists, upgrading is usually cheap. --ignore-unfixed hides findings you cannot act on yet.
  2. Is it reachable? A vulnerable library function your code never calls, in a package that is only present because the base image is large, is a lower priority than a flaw in your request-parsing path.
  3. Can you remove it entirely? Moving to a slim or distroless base deletes whole categories of findings, which is the security payoff of the multi-stage builds from the Docker module.
Watch out

Gate on a threshold you will actually enforce, such as fixable HIGH and CRITICAL findings. A gate that fails every build gets disabled within a week. Record accepted risks in an ignore file with a reason and an expiry date, reviewed like code.

SBOMs, signing and provenance

A software bill of materials (SBOM) is a machine-readable inventory of every component in an artifact, with versions and licences. The two common formats are SPDX and CycloneDX. Its value shows on the day a major vulnerability is announced: instead of rebuilding and scanning everything, you query the SBOMs you stored and know within minutes which services contain the affected library.

bash
syft ghcr.io/acme/orders-api:1.4.2 -o spdx-json > sbom.spdx.json
trivy image --format cyclonedx --output sbom.cdx.json ghcr.io/acme/orders-api:1.4.2

grype sbom:./sbom.spdx.json          # scan the inventory later, without the image

Scanning tells you what is inside. Signing tells you where it came from. The pipeline signs the image digest after building it, and the deploy target verifies the signature before running anything, so an image pushed to your registry by someone other than your pipeline is rejected. Sigstore's cosign is the common open-source tool, and it supports keyless signing tied to the pipeline's OIDC identity.

bash
cosign sign ghcr.io/acme/orders-api@sha256:9b2c0f5e...      # sign the digest, not a tag
cosign verify ghcr.io/acme/orders-api@sha256:9b2c0f5e... \
  --certificate-identity-regexp 'https://github.com/acme/.*' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com
imagegate passedrecordspush + signaturepullsignature valid"are we affected?"Sourcelockfile, pinned baseCI buildreproducibleScan + SBOMtrivy, syftSign digestcosign, OIDCSBOM storeper releaseRegistryimage@sha256:...Verifyadmission policyClusterruns by digest
A defended supply chain: pinned dependencies go into a reproducible build, the artifact is scanned and its SBOM recorded, the digest is signed, and the deploy target verifies the signature before it will run anything.

Provenance goes one step further: a signed statement of how the artifact was built, from which commit, by which workflow. The SLSA framework describes increasing levels of this assurance. You do not need all of it on day one. A sensible order is: lockfiles and pinned bases, scanning in CI, SBOMs stored per release, then signing with verification at deploy.

Common supply-chain attacks

  • Typosquatting: a malicious package named almost like a popular one, waiting for a typing mistake in an install command.
  • Dependency confusion: your internal package name is registered on the public registry with a higher version, and the package manager prefers it. Defend with scoped or namespaced packages and by configuring which registry serves which names.
  • Compromised maintainer or build: a legitimate package publishes a malicious release. Lockfiles with hashes stop it arriving silently; reviewed update pull requests give you a chance to notice.
  • Mutable references: a tag or an unpinned GitHub Action changes underneath you. Pin images by digest and third-party actions by commit SHA.

Pulling everything through a pull-through cache or private proxy registry gives you one place to enforce these rules, and it also protects your builds from upstream outages and rate limits.

Hands-on practice

Make one image traceable, reproducible and inspected

  1. Extend the pipeline from the CI/CD module so every image is tagged with the short commit SHA. After a run, find the image's digest with docker buildx imagetools inspect.
  2. Pull the same image once by tag and once by digest. Then push a different build to the same tag and show that the tag now resolves to a new digest while the old digest still pulls the old image.
  3. Make sure the project has a committed lockfile and that CI uses the strict install command (npm ci, pip install --require-hashes, or cargo build --locked). Break the lockfile on purpose and watch CI fail.
  4. Install Trivy and scan your image. Count the findings, switch the Dockerfile to a slim or distroless base, rebuild, and compare.
  5. Add a pipeline step that runs Trivy with --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1, and an ignore file containing one documented, dated exception.
  6. Generate an SBOM with Syft or Trivy, store it as a build artifact, and use jq to list every package name and version it contains.
  7. Enable Dependabot or Renovate on the repository and merge the first update pull request it opens once CI is green.
Cheat sheet

Artifacts & supply chain — at a glance

Main things to focus on

  • Tags are mutable labels; digests are immutable content hashes. Deploy what you tested by SHA tag or digest, never latest.
  • Semantic versioning: MAJOR breaks, MINOR adds, PATCH fixes.
  • Promote by re-tagging the built image. Never rebuild for a release.
  • Commit lockfiles and use strict installs in CI. Most of your code is transitive dependencies.
  • Pin, then automate updates with a bot, so you get both reproducibility and patches.
  • Scan on every build and re-scan deployed images on a schedule. Gate on fixable HIGH and CRITICAL.
  • Keep an SBOM per release so you can answer "are we affected?" in minutes.
  • Sign digests in the pipeline and verify at deploy to prove where an image came from.

References and versions

REGISTRY/REPO:TAGMutable, human-friendly reference
REGISTRY/REPO@sha256:DIGESTImmutable, content-addressed reference
MAJOR.MINOR.PATCHBreaking . feature . fix
^1.4.2npm range: any 1.x.y at or above 1.4.2
~1.4.2npm range: any 1.4.x at or above 1.4.2
git rev-parse --short HEADShort commit SHA for an image tag

Registry commands

docker login REGISTRYAuthenticate (use --password-stdin in scripts)
docker tag SRC:TAG REGISTRY/REPO:TAGAdd another name to an existing image
docker push REGISTRY/REPO:TAGUpload
docker pull REGISTRY/REPO@sha256:DIGESTPull an exact image
docker buildx imagetools inspect REFShow manifest, digest and platforms

Strict, reproducible installs

npm ciInstall exactly the lockfile; fail if it is out of sync
pip-compile --generate-hashesProduce a fully pinned, hashed requirements file (pip-tools)
pip install --require-hashes -r requirements.txtRefuse any package whose hash does not match
go mod verifyCheck downloaded modules against go.sum
cargo build --lockedFail if Cargo.lock would need to change
FROM image:tag@sha256:DIGESTPin a base image

Scan, inventory, sign

trivy image REFVulnerabilities in OS and language packages
trivy image --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 REFPipeline gate
trivy fs . / trivy config .Scan lockfiles / Dockerfile and IaC misconfigurations
syft REF -o spdx-jsonGenerate an SBOM
grype sbom:./sbom.spdx.jsonScan a stored SBOM
cosign sign REPO@sha256:DIGESTSign an image digest
cosign verify REPO@sha256:DIGESTVerify a signature before deploying

Common pitfalls

  • Deploying latest, then being unable to say which version is running or to roll back to the previous one.
  • Leaving the lockfile out of Git, so every build resolves a different dependency tree.
  • Pinning everything and never updating, which freezes known vulnerabilities in place.
  • Setting a scan gate so strict that it is switched off instead of tuned.
  • Scanning only at build time, while images that have run for months accumulate new CVEs unnoticed.
  • Signing a tag instead of a digest, which says nothing once the tag moves.
Quiz

Check your understanding

5 questions · 4 to pass · answers are explained as you go. Your best score is saved on this device only.

Progress and quiz scores are saved in this browser only. Back up or restore on the hub.

Was this lesson useful? Tell me what to improve →