Your application is a thin layer of your own code on top of thousands of packages written by strangers, built by a pipeline that runs code from more strangers, and shipped as an image assembled from yet more. Attackers noticed. Poisoned packages, hijacked maintainer accounts, compromised build servers and tampered artifacts are now routine, and a single one reaches every customer of every company that depends on it. Supply chain security is about knowing exactly what you ship, proving where it came from, and refusing to run anything that cannot prove it. This module covers dependencies, SBOMs, signing with Sigstore, provenance and the SLSA levels, and protecting the build itself.
- Pin and audit dependencies, and reduce the number of them
- Generate a software bill of materials and use it to answer "are we affected?" in minutes
- Sign artifacts and images keylessly with Sigstore and verify signatures before deployment
- Explain SLSA levels and produce build provenance from a hosted pipeline
- Harden the build pipeline: pinned actions, minimal permissions, isolated runners, protected branches
Dependencies: fewer, pinned, watched
Every dependency is code you run with your privileges, maintained by someone you have never met, published through an account that can be phished. The attacks are well known: typosquatting (a package one letter away from the real one), dependency confusion (a public package with the same name as your internal one, and a resolver that prefers the higher version), hijacked maintainer accounts publishing a malicious release, and install-time scripts that run on npm install.
- Fewer: every dependency you remove is a supplier you no longer have to trust. Question transitive bloat.
- Pinned: lock files with exact versions and hashes (
package-lock.json,poetry.lock,requirements.txt --require-hashes,go.sum) so an install today and next month are identical. - Sourced: a private registry or proxy that mirrors approved packages, blocks unknown ones, and makes dependency confusion impossible by design.
- Watched: automated updates (Renovate, Dependabot) with tests, and vulnerability scanning on every change and on a schedule.
- Quiet: disable install scripts where you can (
npm ci --ignore-scripts) and review packages that need them.
# generate a fully pinned, hashed requirements file from your top-level deps
pip-compile --generate-hashes requirements.in -o requirements.txt
# installs fail if any package's hash does not match the lock
pip install --require-hashes -r requirements.txt
# known vulnerabilities in exactly what is pinned
pip-audit -r requirements.txt --strictA new dependency deserves the same review as new code: who maintains it, how many maintainers, when was it last released, does it need install scripts, what does it pull in transitively. Popular is not the same as safe, but abandoned is a clear signal.
Know what you ship: the SBOM
A software bill of materials is a machine-readable inventory of every component in an artifact: packages, versions, licences, and their relationships, in a standard format (SPDX or CycloneDX). Its value is speed under pressure. When the next critical vulnerability in a logging library is announced, teams with SBOMs answer "which of our two hundred services contain version 2.14.1?" with a query; teams without spend days grepping. Generate an SBOM for every build, store it next to the artifact, and index it.
# SBOM in CycloneDX JSON, attached to the build as an artifact
syft ghcr.io/acme/orders:sha-1a2b3c4 -o cyclonedx-json > sbom.cdx.json
# scan the SBOM (no need to pull the image again) for known vulnerabilities
grype sbom:sbom.cdx.json --fail-on high
# the question that matters during an incident
jq -r '.components[] | select(.name=="log4j-core") | .version' sbom.cdx.jsonSBOMs also serve customers and regulators who increasingly ask for them, and they feed licence compliance. Attach the SBOM to the image as an attestation (next section) so that it travels with the artifact and cannot be swapped.
Sign it: Sigstore and keyless signatures
A signature lets a consumer verify that an artifact was produced by a specific identity and not altered since. Traditional signing keys are a liability: they leak, they expire, they get shared. Sigstore removes the long-lived key. The signer proves its identity with an OIDC token (a CI job's identity, or a developer's login), receives a short-lived certificate from the Fulcio CA, signs, and records the signature in the Rekor transparency log. Verifiers check the signature against the certificate and confirm the certificate's identity claims: this image was signed by the release workflow of this repository on the main branch.
permissions:
contents: read
packages: write
id-token: write
steps:
- uses: sigstore/cosign-installer@v3
- name: Sign the image by digest
run: cosign sign --yes "ghcr.io/acme/orders@${DIGEST}"
env:
DIGEST: ${{ steps.build.outputs.digest }}
- name: Attach the SBOM as an attestation
run: cosign attest --yes --predicate sbom.cdx.json --type cyclonedx "ghcr.io/acme/orders@${DIGEST}"
env:
DIGEST: ${{ steps.build.outputs.digest }}cosign verify \
--certificate-identity-regexp '^https://github.com/acme/orders/.github/workflows/release.yml@refs/heads/main$' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ghcr.io/acme/orders@sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08Sign and verify by digest, never by tag. A tag is a mutable pointer; the digest is the content. Deployment manifests should reference digests, and admission should resolve tags to digests and verify those.
Provenance and SLSA
A signature says who signed; provenance says how the artifact was built: which source commit, which builder, which inputs, which build instructions. SLSA (Supply-chain Levels for Software Artifacts) is the framework that grades this. Level 1: provenance exists. Level 2: it is generated by a hosted build platform and signed, so it cannot be forged by the developer. Level 3: the build runs on hardened, isolated infrastructure where the provenance cannot be tampered with by the build's own steps. Most teams reach level 2 or 3 by using a hosted CI's provenance generator rather than building their own.
- uses: actions/attest-build-provenance@v2
with:
subject-name: ghcr.io/acme/orders
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: trueProvenance makes policy possible. A consumer (a cluster admission controller, a package installer) can require: built from a commit on the protected main branch, by the official workflow, on the hosted builder, with the SBOM attached. Combine with verification by digest and you have closed the gap between "we build good software" and "only software we built can run".
Protect the build itself
The pipeline is a privileged system that executes code from pull requests and holds the credentials to publish. Attackers target it directly: a malicious pull request that exfiltrates secrets, a compromised third-party action, a poisoned build cache. Treat CI configuration as security-critical code.
| Risk | Control |
|---|---|
| Third-party actions and images change under you | Pin actions to a commit SHA, images to a digest; review updates via bot PRs |
| Pull requests from forks run with secrets | Fork PRs get no secrets by default; never use pull_request_target with checkout of the PR head |
| Workflow token can push code or publish | permissions: contents: read at the top; grant packages: write or id-token: write only to the job that needs it |
| Untrusted input in shell steps | Pass ${{ github.event.* }} values through environment variables, never interpolate into run: |
| Shared, long-lived runners accumulate state | Ephemeral runners; no persistent credentials on the runner; isolated per job |
| Anyone can change the workflow | Protected branches, required reviews for .github/, CODEOWNERS |
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Greet the PR author safely
env:
TITLE: ${{ github.event.pull_request.title }}
# never interpolate the expression directly into the script
run: |
echo "PR title is $TITLE"Finally, review your own trust decisions periodically: which registries you pull from, which actions you allow (GitHub can restrict to verified creators or an allow-list), which people can approve a release. Supply chain security is not a product; it is knowing your suppliers and making the pipeline the only road to production.