Skip to content

Software Supply Chain Security Explained

Lesson 1 of 10Intermediate → Advanced16 min readGit Security & DevSecOps · Supply Chain SecurityVerified: SLSA v1.2, GitHub artifact attestations and Sigstore documentation, September 2026

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.

From developer to runtime

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.

DeveloperWrites and commitsSource repositoryReviewed, protected, signedDependenciesResolved, pinned, hashedBuild workflowIsolated, reproducible, credentialledArtifactIdentified by digestRegistry / releaseStored, tagged, publishedDeploymentSelected, verified, admittedRuntimeRunning with real access

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

StageTakes on trustMade checkable by
Source repositoryThat commits come from who they claimSigned commits, verified authorship
Dependency resolutionThat a name resolves to the intended artifactLock files with integrity hashes
Build workflowThat the workflow file is what maintainers intendedReview, CODEOWNERS, protected branches
Build executionThat third-party actions are what they wereSHA pinning
Build environmentThat the runner is cleanEphemeral runners, isolation
Artifact storageThat the stored bytes are the built bytesDigest references, signatures
ReleaseThat the published thing has not changedImmutable releases, attestations
DeploymentThat the artifact is the one intendedVerification 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.

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.

ArtefactQuestion it answers
Lock file with hashesDid we get the exact dependency bytes we expected?
SBOMWhat components are inside this artifact?
ProvenanceWhere and how was this artifact built?
SignatureWho produced this, and has it changed since?
Verification policyDo 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.

Not everything at once, and the order is not the order of the chain.

  1. 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.

  2. Remove stored cloud credentials from builds. OIDC. The single highest-value change in this pillar and it is a workflow edit.

  3. Build once, promote by digest. If staging and production run different binaries, everything you tested applies to a different artifact.

  4. Generate provenance attestations. A few lines in the build workflow.

  5. Generate an SBOM, once you have somewhere to consume it.

  6. Make releases immutable, so tags and assets cannot move after publication.

  7. 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.

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.

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.

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:

  1. 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.

  2. 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.

  3. 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.

  4. 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”.

  5. Rebuild from a clean tree and redeploy, having pinned away from the affected version.

  6. 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.

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.

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.

  • 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

Mostly analysis. Use a repository you actually build and deploy.

  1. Trace your own chain, stage by stage, and write down what each stage takes on trust.

  2. For your most recently deployed artifact: which commit built it? Predict: can you answer without guessing?

  3. Check your build inputs for mutable references — base images, actions, tool downloads. Predict: how many are pinned by digest or SHA?

  4. Check whether the artifact in production is the one that was tested, or a rebuild.

  5. Ask what would happen if a dependency you use published a malicious version tonight. Which control catches it, and when?

  6. Ask whether anything in your deployment path would refuse an artifact that failed verification. Predict: the answer for most organisations is no.

  7. Pick the cheapest gap from the list and close it this week.

GitHub Actions Security ChecklistToken permissions, fork pull requests, script injection and supply chain — with the attack each item prevents.

The repository security templates — secrets management and least-privilege token guides — are in the Professional Toolkit.