Software Supply Chain Security and DevSecOps
Consider a hypothetical release that goes out on a Tuesday and behaves normally. Two weeks later, an engineer chasing an unrelated bug notices a package in the running image that nobody remembers approving. It carries the name of an internal utility, and its version is 9.9.9.
Someone published that internal name on a public index. The build searched both internal and public sources; its loose version requirement admitted 9.9.9, the package was compatible, and no approved artifact hash excluded it. The resolver selected the malicious source distribution. Its build backend ran a hook with access to a registry push token and a repository write token on the build machine. A normal wheel installation does not run this source-build hook; malicious wheel contents can instead execute when imported or invoked later.
Three checks had passed. Review covered source changes and loose dependency declarations, but no resolved dependency diff was retained for approval. The known-vulnerability scan found no matching advisory for the injected code. Signature checking succeeded because the pipeline signed the artifact it had built. Those results were consistent with the attack.
Each check answered its own question. None of them answered the question that mattered: is this artifact the thing we chose, built the way we intended, from the source we reviewed? Getting that answered is what the practices in this area are for, and the way to organize them is not as a list of tools but as four transitions where something changes hands.
Four transitions, four different checks
| Transition | What changes hands | What has to be established | If it is not |
|---|---|---|---|
| Source to build | Reviewed code becomes an input to an automated process | Which revision, approved by whom, and which pipeline definition is permitted to build it | An artifact can be built from a branch or fork nobody reviewed |
| Build to artifact | Inputs are resolved and an output is produced | Which dependencies were actually resolved, and what the build environment could reach | Contents arrive that appear in no diff, as in the opening |
| Artifact to registry | The output becomes a named, retrievable thing | Identity by digest, a signature, and a record of how it was produced | The name says nothing; a tag can point somewhere else tomorrow |
| Registry to runtime | Something is selected and executed | The consumed digest matches evidence and policy checked through a protected deployment or admission path | A bypass path or later substitution can defeat an earlier check |
Reviewing source and signing outputs leaves a gap if no control compares actual build inputs with approved dependencies. In this example the wrong package entered during resolution; the deployment path then accepted the resulting artifact.
What review and analysis can establish
Code review examines the proposed change and its intended behavior; approval is evidence of review, not proof of correctness. SAST examines code for classes of defects within the analysis it supports. Dependency review examines changes in components, versions, licenses, and known vulnerabilities. Their inputs and coverage differ.
A committed lock file can expose resolved direct and transitive dependencies before the build. GitHub dependency review uses dependency changes available to its graph, rather than being restricted to a loose manifest declaration. The blind spot arises when reviewers see only a package name and version range, while the later resolution is neither retained nor compared with what they approved.
Review the resolved evidence when available, then check that the build actually used the approved inputs. A lock file can be ignored or supplemented by a build script, so approval of that file alone does not establish the contents of the resulting artifact.
Dependencies: name resolution is an attack surface
The pip install documentation explains that configured package locations are searched without assigning an index priority; pip selects a best acceptable candidate across them. A public package can therefore compete with an internal package of the same name, provided it satisfies the requirement and compatibility checks. The mechanism, how internal names leak, and the controls are covered in dependency confusion.
Origin policy and artifact pinning address different decisions. A single proxy helps centralize policy only if internal names or namespaces are bound to approved origins, public upstream fallback is blocked for those names, and direct bypass routes are controlled. New internal names need the same protection. AWS CodeArtifact documents origin controls separately from the repository endpoint. For pip, pin versions or exact artifact references and use --require-hashes with approved local hashes for every requirement, including dependencies. The Secure installs guidance distinguishes those hashes from hashes supplied by the index serving the file: an index-supplied checksum is not independent approval. The pinned record itself is the subject of the dependency lock file.
Hash checking rejects files outside the approved set; it cannot tell whether approving that set was a good decision. Review new packages and upgrades before accepting their hashes. The count of newly accepted direct dependencies can help plan that review workload, but it is not a substitute for assessing provenance, maintainership, behavior, exposure, and known vulnerabilities.
Images: identity is a digest, not a name
A tag is a label someone can move. A digest is a hash of content, so it names exactly one thing forever; the specification vocabulary for artifacts makes the same point when it defines an artifact as “an immutable blob of data”. The practical consequence is that a deployment referring to a tag is a deployment whose contents can change without any change to the deployment, and the fix is to resolve the tag once, record the digest, and deploy the digest. How images and layers are structured is covered in container image and the format work in OCI, with the boundaries a container does and does not draw in containers, images, and their boundaries.
Mutable references affect pipeline components as well as images. GitHub recommends pinning an action to a full-length commit hash so that a repository owner cannot replace the selected release simply by moving a tag. An attacker with repository access can move or delete a tag, so review the pinned commit and any deliberate update.
Pinning a base image by digest fixes one build input. It does not make the whole build deterministic: network fetches, timestamps, randomness, and toolchain behavior can still vary. Update the pin deliberately, rebuild a candidate, validate it, and deploy the accepted result. Following a tag alone guarantees neither a fresh pull nor a security fix. Test reproducibility separately. The components observed in the result, including inherited base contents, belong in a software bill of materials.
The pipeline is production
A compromised action can use credentials passed to its job and may obtain other credentials through shared runner state or persistence. GitHub’s Secure use reference warns about interaction between jobs, but exposure depends on isolation and what each job can access. The effective GITHUB_TOKEN permissions determine whether that token can write; the presence of a secret somewhere in repository settings does not mean every action receives it.
A repository writer who can change workflows may be able to arrange additional secret access in a later run. Assess that capability separately from the credentials exposed to one running action. Trigger restrictions, protected workflow changes, environment required reviewers, and the cloud provider’s federation trust policy can limit which jobs receive authority. Shared persistent runners can undermine those boundaries if untrusted work leaves code for a later privileged job.
Four measures follow, and all four are ordinary applications of ideas that apply anywhere else in an estate.
- Least privilege on the pipeline’s own identity, scoped per job rather than per repository, and short-lived rather than permanent. That is least privilege applied to a machine, with the duration argument in standing privilege.
- Pinned, reviewed third-party steps. A workflow that pulls a step by tag executes whatever that tag resolves to; review the pinned commit and updates to it.
- Untrusted input handled as data. Pass external values through an intermediate environment variable, as GitHub recommends, rather than inserting expressions into shell source. Quote expansions and avoid evaluating their contents as commands.
- A protected verification and deployment path. Restrict who can change verifier code, trusted signers, policy, and approval evidence, and who can deploy while bypassing the check. Separate identities can reduce authority, but they do not help if one can alter or impersonate the other. A shared orchestration system can still enforce a check whose controls and deployment credentials the build cannot bypass.
The environment doing the work is worth treating as an asset in its own right, which is the subject of build agents; the definitions it runs belong in version control like any other policy, which is the argument in pipeline as code and policy as code; and the credentials it holds are the subject of secrets management. Where the pipeline’s own authority sits relative to data paths is developed in The Network Is Not a Credential.
Artifacts: three records that answer three questions
Once an artifact exists, three different records get conflated in conversation, and keeping them apart is what makes a verification story coherent.
A signature authenticates a signer’s statement about specified bytes when the key or identity is trusted. Build provenance records how an artifact was produced; SLSA v1.1 defines a structure for comparing the claimed builder, inputs, and process with consumer expectations. An SBOM, as described by NIST and CISA, inventories software components and their relationships. These records support different checks and none alone establishes that the code is safe.
Even an approved source revision and trusted builder can resolve a malicious package under permitted settings and produce truthful provenance. Checking that provenance’s authenticity and artifact digest does not reject the package unless policy also constrains dependency origin, approved versions or hashes, and the actual resolution evidence. Dependency reporting may be incomplete, so obtain additional build evidence where needed. An inventory likewise identifies components without deciding vulnerability applicability; CISA describes VEX as the separate record for that assessment. The structures are covered in build provenance, and independent recomputation in reproducible build.
Verification coverage depends on both timing and authority. Build-time checks can catch input or output mismatches early. Registry ingestion checks can reject unacceptable artifacts, but do not cover later registry or transit compromise. Consumer-side deployment or admission checks can bind the bytes about to run to policy, provided bypass paths are closed. A check inside a pipeline is useful when the attacker cannot alter its verifier, evidence, trust roots, or enforcement; its location alone does not make it circular. The enforcement boundary is developed in policy enforcement point. Artifact identity and promotion are covered in build artifact and artifact promotion across environments; broader controls in software supply chain security.
What tells you it works
For a service running in production, ask which digest is running, which commit and platform produced it, what components it contains, and how their actual origins were checked against approval. Being able to answer supports investigation; it does not itself show that malicious inputs were excluded.
Six measurements follow from trying to answer that.
- Share of running workloads identified by digest rather than by tag, since everything else in the chain binds to the digest.
- Share of deployments whose provenance is verified before running, and where the check happens.
- Reachable package sources and enforced origin rules, including proxy upstreams, internal namespace bindings, fallback behavior, and bypass routes.
- Share of dependencies pinned by hash, and the count of new direct dependencies accepted last quarter.
- Time to answer are we affected by this component, measured on a real advisory rather than estimated.
- What a compromised build could reach: effective permissions, credential lifetime, shared runner state, and the ability to change or bypass verification and deployment policy. The audit log records actions for reconstruction.
A green known-vulnerability scan reports findings within its detection coverage. A signature helps only when authenticated and checked against an expected signer and artifact. An inventory supports estate-wide questions when it can be queried and joined to deployments. Review can include resolved dependencies, but its approved inputs still need comparison with the actual build. These controls contribute evidence; none substitutes for the whole chain.
The sentence to be able to say
For a named production service, the evidence should support this account: the running digest matches an authenticated record from an expected builder, source revision, and process. Actual dependency origins and approved versions or hashes were checked, with gaps in capture made explicit. The component inventory is tied to that digest and joined to deployments. Build authority cannot change or bypass the protected release decision, and credential permissions and lifetimes are limited to their jobs.
In the hypothetical incident, the resolver was allowed to select an unapproved public package. An authentic provenance record could have described that build accurately, and a gate requiring only its presence would still have passed it. Blocking the example requires enforcing internal-name origin policy and approved input checks, then comparing actual resolution and artifact evidence with those expectations. The known-vulnerability scan in the example had no matching advisory; that is a limit of that check, not proof that every form of analysis would miss the attack.
References
- pip documentation, pip install — how locations are searched and matches selected, and the dependency confusion warning.
- pip documentation, Secure installs — hash-checking mode and the limits of hashes obtained from an index.
- GitHub Docs, Secure use reference (GitHub Actions) — job interaction, secret exposure, repository write access, pinning actions to a commit hash, and handling untrusted input.
- SLSA v1.1, Provenance
- SLSA v1.1, Terminology
- NIST Computer Security Resource Center Glossary, software bill of materials. The definition is attributed on the page to NIST SP 800-161r1-upd1, drawing on Executive Order 14028.
- CISA, Software Bill of Materials (SBOM) — the nested inventory description and Vulnerability Exploitability eXchange documents.
- AWS CodeArtifact, Package origin controls
- pip documentation, Build System Interface
- Python Packaging, Binary distribution format
- GitHub Docs, Dependency graph
- GitHub Docs, Dependency review
- SLSA v1.1, Verifying artifacts
- SLSA v1.1, Threats overview
- Reproducible Builds, Definition
Discover more from Insightful Data Lab
Subscribe to get the latest posts sent to your email.
