Your users do not run your source code. They run an artifact — a binary, a container image, a package — produced by a process, from inputs, most of which you did not write.
Software supply chain security is about that gap. Specifically:
Can anyone establish that the artifact running in production was built from the source that was reviewed, by the system you believe built it, from the dependencies you intended?
For most organisations the honest answer is “the CI job said so”, which is an assertion by the system whose compromise you would most want to detect.
The chain
Section titled “The chain”A vertical chain: developer, source repository, dependencies, build workflow, artifact, registry or release, deployment, runtime. Each stage receives something, transforms it, and hands it on.
Read it as a chain of custody. Each stage receives something, does something to it, and hands it on. Security is a property of the handoffs: what does each stage assume about its input, and what evidence does it produce that the next one can check?
The framing matters because it makes the shape of the problem visible. Compromise at any stage propagates to everything downstream, and the further upstream it happens, the less likely anyone is to notice — because everything downstream is working exactly as designed.
Stage by stage
Section titled “Stage by stage”Developer
Section titled “Developer”Threat. A developer’s machine or credentials are compromised, and commits are made in their name.
Why it matters here. This is the deepest point of entry. A commit from a trusted maintainer gets different scrutiny in review, and everything after it treats the commit as legitimate input.
Controls. Signed commits so authorship is verifiable; least-privilege access; hardware-backed keys; two-factor authentication.
Source repository
Section titled “Source repository”Threat. Malicious code reaches the branch that gets built — through a direct push, an unreviewed merge, or a change to the build configuration itself.
The variant people miss. Changing .github/workflows/ is changing what runs with the
repository’s credentials. It is a privilege change disguised as a configuration change, and it gets
read as configuration in review.
Controls. Rulesets requiring review;
CODEOWNERS on /.github/;
branch protection.
Dependencies
Section titled “Dependencies”Threat. Code you did not write, and mostly did not choose, enters the build. This is the largest attack surface in the chain by volume — hundreds to thousands of packages from hundreds of maintainers.
The specific attacks:
Compromised package. A legitimate package’s maintainer account is taken over, or a maintainer turns malicious, and a version containing a payload is published under a name you already trust. Version pinning is the defence, and it is only a defence until you upgrade.
Typosquatting. A package published under a name close to a real one — a transposition, a hyphen, a singular where the real one is plural. Installed by a typo, or by a copied command.
Dependency confusion. The one worth understanding in detail, because it exploits tooling
behaviour rather than human error. If your build resolves packages from both a public registry and an
internal one, and your internal package acme-utils is not published publicly, an attacker publishes
acme-utils to the public registry with a very high version number. Package managers configured to
prefer the highest version across all configured sources fetch the attacker’s package. Nobody made a
mistake; the resolution logic did what it was configured to do.
Controls. Lock files with integrity hashes; scoped internal namespaces with
deny-groups in dependency review; private registries
configured so internal names never resolve publicly; Dependabot for
known vulnerabilities; cooldown periods so you are not the first to adopt a fresh release.
Build workflow
Section titled “Build workflow”Threat. The build produces something other than what the source specifies.
This is the highest-value target in the chain, for a reason worth stating plainly: compromising the build is better than compromising the source, because the build output is what people install and it is what nobody reads. A backdoor in source has to survive review. A backdoor injected at build time does not exist in any file anybody looked at.
The specific routes:
A malicious action. A third-party action referenced by a moving tag is code you execute with your build’s credentials. If the tag moves, so does what runs. See Pinning actions.
Workflow injection. Untrusted input — a pull request title, a branch name — substituted into a
run: block becomes program text. See
Workflow security.
A compromised runner. A self-hosted runner that persists between jobs is a machine an attacker can live on, with access to everything subsequent jobs handle. See Secure self-hosted runners.
Stolen build credentials. A long-lived cloud key in a repository secret works from anywhere, forever, for whoever obtains it.
Controls. Pinned actions by SHA; minimal permissions:; ephemeral runners;
OIDC instead of stored credentials; build isolation; and
provenance so a build that did not happen the way you
expect is detectable afterwards.
Artifact
Section titled “Artifact”Threat. The artifact is replaced or modified between build and deployment.
The enabler is mutable references. A tag is a pointer. myimage:v1.2.3 can be repointed to a
different image without changing the tag anybody references. A digest — myimage@sha256:... — is the
content, and it cannot be repointed.
Controls. Reference by digest everywhere it matters; signatures so tampering is detectable; attestations so origin is verifiable.
Registry and release
Section titled “Registry and release”Threat. The published thing changes after publication, or an additional thing is published under your identity.
Controls. Registry access control; immutable releases so a release’s tag and assets are locked; signing; signed container images.
Deployment
Section titled “Deployment”Threat. Something other than the intended artifact is deployed.
This is where the entire chain either pays off or does not. Every attestation, signature and SBOM produced upstream is inert unless something at deployment refuses artifacts that fail verification.
Controls. Admission policies that verify attestations; deployment by digest; environments with required reviewers.
What an attacker actually wants
Section titled “What an attacker actually wants”Naming the objective makes the defences make sense, and it explains why upstream stages are worth so much more attention than they usually get.
Reach. One compromised package in a widely-used library reaches every consumer. One compromised application reaches its users. The ratio is why supply-chain attacks are economically attractive in a way that attacking individual targets is not.
Invisibility. A backdoor in an artifact nobody reads, produced by a build everyone trusts, running in a system whose logs look normal. The best supply-chain compromises are discovered months later, by somebody else.
Persistence. Compromise the build and every future release carries the payload. Compromise one deployment and you have one deployment.
The pattern across all three: the further upstream, the better the attack. That is the argument for spending effort on source and build integrity rather than only on scanning the output.
Attack patterns worth recognising
Section titled “Attack patterns worth recognising”Described as shapes rather than as incidents, because the shape is what recurs.
The maintainer transition. A widely-used package has a maintainer who has lost interest. Somebody helpful appears, contributes for months, and is granted publish rights. A later release contains a payload. Nothing about the sequence looks wrong at any point, and the package’s popularity is what made it a target.
What helps: cooldown periods before adopting new releases, so you are not the first to find out; dependency review’s age and adoption signals; and being suspicious of a package suddenly gaining new maintainers or a first release in two years.
The build injection. Rather than modifying source, an attacker modifies the build — a workflow change, a malicious action, a compromised build tool. The source in the repository is exactly what everybody reviewed. The artifact is not what the source produces.
What helps: CODEOWNERS on build configuration; pinned actions; provenance, which makes “this artifact was built by a workflow you do not recognise” a detectable statement rather than an unanswerable question.
The dependency confusion resolve. Described above. Worth repeating that no human error is involved — the package manager resolves the highest available version across configured sources, and an attacker publishes a very high version of your internal package name publicly.
What helps: scoped namespaces, registry configuration that never falls back to public for internal names, and blocking your namespace in dependency review.
The tag repoint. An artifact is published, verified and deployed. Later, the tag is moved to a different artifact. Everything that references the tag now gets something else, and nothing about the reference changed.
What helps: digests everywhere, immutable releases, tag rulesets.
The unnoticed transitive. A direct dependency updates. Its lock file entry brings in a new transitive dependency nobody has heard of. The pull request title says “bump the HTTP client”.
What helps: reading the indirect changes in dependency review, which is the one control positioned to catch this before it lands.
None of these require sophistication. They require patience and an understanding of how the tooling behaves, which is precisely why the defences are mostly configuration rather than detection.
Trust boundaries
Section titled “Trust boundaries”A useful exercise is to list what each stage takes on trust, because the list is longer than people expect and the entries are where the controls belong.
| Stage | Takes on trust | Made checkable by |
|---|---|---|
| Source repository | That commits come from who they claim | Signed commits, verified authorship |
| Dependency resolution | That a name resolves to the intended artifact | Lock files with integrity hashes |
| Build workflow | That the workflow file is what maintainers intended | Review, CODEOWNERS, protected branches |
| Build execution | That third-party actions are what they were | SHA pinning |
| Build environment | That the runner is clean | Ephemeral runners, isolation |
| Artifact storage | That the stored bytes are the built bytes | Digest references, signatures |
| Release | That the published thing has not changed | Immutable releases, attestations |
| Deployment | That the artifact is the one intended | Verification policy at admission |
Reading the middle column is the exercise. Every entry is an assumption somebody made once, usually implicitly, and most organisations have never written them down.
Reading the right column tells you what a mature chain looks like: at every boundary, something the next stage can check without trusting the previous one.
Reproducibility, and why it is hard
Section titled “Reproducibility, and why it is hard”The intuitive verification is “rebuild it and compare”. It mostly does not work, and understanding why explains the existence of provenance.
Two builds of the same commit usually differ. Timestamps embedded in archives. Absolute paths from the build directory. File ordering from a filesystem. Compiler and toolchain versions. Randomised hash seeds. Locale and timezone. None of these are bugs; they are ordinary properties of build tooling that was never asked to be deterministic.
Reproducible builds are the practice of eliminating those sources of variation so that the same input reliably produces byte-identical output. Where achieved, it is the strongest verification available: anybody can rebuild and compare, and no trust in the builder is required.
It is also genuinely hard, requires ecosystem-wide effort, and is achieved by a minority of projects.
Provenance is the pragmatic alternative. Rather than proving the artifact is reproducible, record verifiable facts about how it was produced — which source, which workflow, which builder — signed by something the builder cannot forge. That does not eliminate trust in the build platform; it makes the platform’s claims checkable, and it is achievable with a workflow step rather than an engineering programme.
Practical steps toward determinism are worth taking anyway, because they help debugging as much as
security: SOURCE_DATE_EPOCH for timestamps, pinned toolchain versions, sorted file ordering, and
fixed locale. See Docker CI for the container version.
The evidence that makes handoffs checkable
Section titled “The evidence that makes handoffs checkable”Five artefacts recur, they are routinely conflated, and each answers exactly one question.
| Artefact | Question it answers |
|---|---|
| Lock file with hashes | Did we get the exact dependency bytes we expected? |
| SBOM | What components are inside this artifact? |
| Provenance | Where and how was this artifact built? |
| Signature | Who produced this, and has it changed since? |
| Verification policy | Do we accept this artifact, given the above? |
The last row is the only one that is a control. The first four are evidence, and evidence nobody evaluates is documentation.
Where to start
Section titled “Where to start”Not everything at once, and the order is not the order of the chain.
-
Pin your dependencies. Lock files with integrity hashes, actions by SHA, base images by digest. Cheap, immediate, and it closes the largest-volume attack surface.
-
Remove stored cloud credentials from builds. OIDC. The single highest-value change in this pillar and it is a workflow edit.
-
Build once, promote by digest. If staging and production run different binaries, everything you tested applies to a different artifact.
-
Generate provenance attestations. A few lines in the build workflow.
-
Generate an SBOM, once you have somewhere to consume it.
-
Make releases immutable, so tags and assets cannot move after publication.
-
Verify at deployment. The step that converts 4 to 6 from artefacts into controls, and the step most organisations never reach.
Steps 1 to 3 are worth doing regardless of any supply-chain programme. Step 7 is where the security benefit actually lands.
The internal supply chain
Section titled “The internal supply chain”Supply chain discussion tends to assume public open-source packages. For most organisations a large part of the chain is internal, and it is often less protected than the public part — because “we wrote it” reads as “we trust it”.
An internal package is a supply chain link with all the same properties:
It has maintainers, whose accounts can be compromised, and who leave.
It has a registry, whose access control is frequently weaker than a public registry’s — no two-factor requirement, publish rights granted broadly, no signing.
It is trusted more. Internal packages routinely skip the review a public dependency would get, because they came from a colleague. That inverts the actual risk: a compromised internal package is adopted faster and questioned less.
It is invisible to external tooling. No advisory database covers your internal packages. No scanner has a detector for them. If a malicious version is published internally, the only thing that finds it is your own review.
The controls are the same ones applied inward:
- Publish rights are access control. Who can push to the internal registry is the same question as who can push to a repository, and it deserves the same treatment.
- Version pinning applies internally too. A floating version range on an internal package is the same risk as one on a public package, with less scrutiny.
- Provenance for internal artifacts. Attestations are as useful pointing inward as outward, and the consumer is a team you can actually get to verify them.
- Namespace protection. Reserve your internal names publicly, or configure resolution so internal names never fall back to a public source. This is the dependency confusion defence, and it is configuration rather than vigilance.
The two directions
Section titled “The two directions”Almost every organisation is both, simultaneously, and the two roles need different work.
As a consumer, you install packages, pull base images and run third-party actions. Your controls are pinning, integrity hashes, dependency review, private registries, and verifying what you can before use.
As a producer, something you build is installed by somebody else. Your controls are the ones that let them verify without trusting you: published provenance, signed artifacts, immutable releases, an SBOM.
The asymmetry worth noticing is that producing evidence is easy and consuming it is a habit. Adding an attestation step to a build takes an afternoon. Making every deployment refuse artifacts that fail verification means changing a process people rely on under pressure — and that is the half that delivers the security benefit.
If you do one direction first, do the consumer side. Pinned dependencies and verified inputs protect you today. Published attestations protect somebody else, later, if they bother to check.
Responding to a suspected compromise
Section titled “Responding to a suspected compromise”The framing above is preventive. It is worth knowing what the incident looks like, because it is unusual in one respect: you frequently cannot tell what is affected.
When a dependency you use is found to have shipped a malicious version, the questions are:
-
Did we ever resolve the affected version? Your lock file history answers this precisely, which is one of the underappreciated arguments for committing it. Without one, you are inferring from version ranges and build dates.
-
Which artifacts were built while it was in the tree? Provenance answers this directly: query attestations for builds in the window. Without provenance, the answer is derived from CI logs, if they are retained that long.
-
Where did those artifacts get deployed? Deployment records, if the deployment references a digest. If it references a tag, the tag may since have moved.
-
What did the malicious code do? Usually published by the time you are asking, and it determines whether the response is “rebuild and redeploy” or “assume the build environment was compromised and rotate everything it could reach”.
-
Rebuild from a clean tree and redeploy, having pinned away from the affected version.
-
Rotate anything the build could reach, if the payload had build-time execution. This is the expensive branch, and it is the reason minimal build credentials matter — the rotation scope is whatever the build held.
Steps 1 to 3 are answerable in minutes by an organisation with lock files, provenance and digest-based deployment, and take days without them. That difference is the practical case for the whole chapter: the evidence is not primarily for proving things to outsiders, it is for answering your own questions during an incident.
Common mistakes
Section titled “Common mistakes”Treating this as a scanning problem. Scanning finds known vulnerabilities in what you have. It says nothing about whether the artifact is what you built.
Mutable references for build inputs. A base image at :latest, an action at @main. Your build’s
inputs change with no change on your side, and the provenance faithfully records that you built from
something you cannot reproduce.
Rebuilding for production. Different binaries in staging and production means your testing applies to a different artifact.
Generating evidence nobody consumes. SBOMs in a bucket and attestations nobody verifies are cost without control.
Believing attestation implies safety. It attests to origin. Vulnerabilities are a separate question.
Securing the source and ignoring the build. The build is the higher-value target precisely because its output is not reviewed.
Assuming private means safe. A compromised internal package is a supply-chain attack with a shorter blast radius, not a different category.
Mental model
Section titled “Mental model”The supply chain is a chain of custody. Each stage takes an input on trust and produces an output somebody else trusts. Security means making each handoff checkable — and then actually checking, at the one point where a failed check can stop something.
What you learned
Section titled “What you learned”- The chain runs developer, source, dependencies, build, artifact, registry, deployment, runtime
- Compromise propagates downstream, and the further upstream it happens the less visible it is
- Compromising the build beats compromising the source, because build output is not reviewed
- Dependency confusion exploits resolution logic, not human error, and is prevented by namespace control
- Mutable references — tags, branches,
:latest— are what make artifact substitution possible - Lock files, SBOMs, provenance and signatures are evidence; only a verification policy is a control
- An attestation proves origin and says nothing about vulnerabilities
- Start with pinning, OIDC and build-once-promote; finish with verification at deployment
- Every organisation is both consumer and producer, and the consumer side protects you first
Exercise
Section titled “Exercise”Mostly analysis. Use a repository you actually build and deploy.
-
Trace your own chain, stage by stage, and write down what each stage takes on trust.
-
For your most recently deployed artifact: which commit built it? Predict: can you answer without guessing?
-
Check your build inputs for mutable references — base images, actions, tool downloads. Predict: how many are pinned by digest or SHA?
-
Check whether the artifact in production is the one that was tested, or a rebuild.
-
Ask what would happen if a dependency you use published a malicious version tonight. Which control catches it, and when?
-
Ask whether anything in your deployment path would refuse an artifact that failed verification. Predict: the answer for most organisations is no.
-
Pick the cheapest gap from the list and close it this week.