On this page
- You didn't write most of your production code
- Shift left: security in the pipeline, not after it
- SBOM: an ingredients label for your software
- Signing and provenance: prove it's really yours
- SLSA: a maturity ladder for your build
- Least-privilege CI and dependency hygiene
- Common mistakes that cost hours
- Takeaways
- Where to go next
You didn't write most of your production code
Open your package.json or go.mod and count the direct dependencies. Now count the *transitive* ones, the dependencies of your dependencies. It's usually hundreds, often over a thousand. Add your base container image, your build tools, your CI runner, and the plugins in your pipeline. The uncomfortable truth: most of the code running in your production environment was written by strangers, pulled in automatically, and trusted by default.
That's the software supply chain, and it's now the favoured attack surface. SolarWinds, Codecov, the event-stream and xz backdoors, none of these broke in through your code. They came in through something you trusted. Securing the supply chain means treating every input to your build as untrusted until proven otherwise: knowing exactly what's in your artifacts (SBOMs), proving they're unmodified (signing and provenance), and hardening the build itself (SLSA). This article covers all three.
Who this is for
Engineers who own a CI/CD pipeline and want to make it defensible. No security background needed, we define SBOM, provenance, SLSA, and signing from scratch. Familiarity with a pipeline (build โ test โ publish) is assumed.
Shift left: security in the pipeline, not after it
Shifting left means moving security checks as early in the pipeline as possible, catching a vulnerable dependency at pull-request time, not in a pen-test six months after it shipped.
The later you find a problem, the more it costs. A vulnerable library caught by a scanner on the PR is a one-line bump. The same library found in production after an incident is an outage, a disclosure, and a postmortem. So the supply-chain discipline is to push every check as far upstream as it'll go, into the build, the PR, the merge.
SBOM: an ingredients label for your software
A Software Bill of Materials is a complete, machine-readable list of every component in an artifact, every library, version, and license, transitive ones included. You can't secure what you can't see, and when the next log4shell drops, the difference between "are we affected?" taking five minutes versus five days is whether you have SBOMs. Generate one for every build and store it as an artifact.
# Generate an SBOM for a built container image (Syft)
syft registry.example.com/api:v2 -o spdx-json > sbom.spdx.json
# Later: instantly answer 'are we exposed to CVE-2025-XXXX?'
grep -i "log4j" sbom.spdx.json
# Or scan the SBOM itself against the vuln database (Grype)
grype sbom:sbom.spdx.json --fail-on highPro tip
SPDX and CycloneDX are the two standard SBOM formats, pick either; both are widely supported. The win isn't the format, it's the habit: an SBOM per build, stored and queryable, so 'what's affected?' is a grep and not an archaeology project.
Signing and provenance: prove it's really yours
An SBOM tells you what's *inside* an artifact. Signing tells you the artifact is the one you built and hasn't been tampered with since. Without a signature, an attacker who compromises your registry can swap your image for theirs and your cluster will happily run it. Sigstore (via the cosign tool) made signing practical: keyless signing tied to your CI's OIDC identity, with the signature published to a public transparency log.
Provenance goes one step further: a signed statement of *how* the artifact was built, which source commit, which builder, which steps. Together, signature + provenance let a deployer verify "this image came from our repo, built by our CI, from this exact commit" before it ever runs.
# A scan + sign + provenance stage in CI
jobs:
publish:
permissions:
id-token: write # OIDC identity for keyless signing
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t $IMAGE .
- name: Scan before publishing
run: grype $IMAGE --fail-on high # block on high/critical CVEs
- name: Generate SBOM
run: syft $IMAGE -o spdx-json > sbom.spdx.json
- name: Push image
run: docker push $IMAGE
- name: Sign (keyless, via OIDC)
run: cosign sign --yes $IMAGE
- name: Attach SBOM as a signed attestation
run: cosign attest --yes --predicate sbom.spdx.json \
--type spdxjson $IMAGE# Deployers verify the signature and the source repo BEFORE running it
cosign verify $IMAGE \
--certificate-identity-regexp 'https://github.com/acme/.*' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com'
# In a cluster, enforce this automatically with an admission policy
# (e.g. Sigstore policy-controller / Kyverno) so unsigned images are rejected.Signing without verification is theatre
Signing your images does nothing if nothing checks the signature. The value only materialises when an admission controller (policy-controller, Kyverno, or a Connaisseur-style gate) rejects unsigned or wrongly-signed images at deploy time. Sign in CI, then enforce verification in the cluster, both halves, or neither.
SLSA: a maturity ladder for your build
SLSA (Supply-chain Levels for Software Artifacts, said "salsa") is a framework that grades how trustworthy your *build process* is. It's a ladder, each level is a concrete, achievable step, not an all-or-nothing certification. You don't need the top rung; you need to know which rung you're on and climb one.
| Level | What it requires | What it stops |
|---|---|---|
| L1 | Build is scripted; provenance exists | Nothing built by hand / undocumented |
| L2 | Hosted build service; signed provenance | Tampering with provenance after the fact |
| L3 | Hardened, isolated builds; non-falsifiable provenance | A compromised build job forging its own provenance |
The jump from L1 to L2 is mostly "use a real CI service and sign the provenance", achievable in an afternoon. L3 (isolated, ephemeral, hardened build runners) is where you stop a compromised build step from lying about what it produced. Pick a target, measure where you are, and treat the gap as a backlog.
Least-privilege CI and dependency hygiene
Your pipeline is itself a high-value target, it has registry credentials, signing identity, and often cluster access. Treat it like production. And the dependencies it pulls deserve the same suspicion as external input, because that's exactly what they are.
- Pin dependencies by hash, not just version. A version tag can be re-pointed; a hash can't. Use lockfiles with integrity hashes and pin GitHub Actions to a commit SHA, never a moving tag like
@v4. - Scope CI tokens to the minimum. Per-job, least-privilege, short-lived. A workflow that builds shouldn't hold deploy credentials. Prefer OIDC over long-lived secrets.
- Scan dependencies and images on every PR, and fail the build on high/critical CVEs. Make the safe path the default path.
- Use a private proxy/registry for dependencies so a deleted or hijacked upstream package can't break or poison your builds.
- Review what you add. A new dependency is new code from a stranger running with your privileges. Weigh whether you need it at all.
The moving-tag trap
Pinning a GitHub Action to @v4 means you run whatever the maintainer (or whoever compromises their account) points v4 at, automatically, on your next build, with your secrets. Pin to a full commit SHA and update deliberately. This single change closes one of the most common CI attack paths.
Common mistakes that cost hours
- No SBOM, so every new CVE is an archaeology project. When the next log4shell drops, 'are we affected?' should be a grep, not a week of spelunking.
- Signing images but never verifying them. A signature nothing checks is decoration. Enforce verification at deploy time with an admission policy.
- Pinning Actions and base images to moving tags. @v4 and :latest mean you silently run whatever someone repoints them to. Pin by SHA / digest.
- Over-privileged CI. A single broad token turns a compromised build into a compromised registry, signing key, and cluster. Scope tokens per job; prefer OIDC.
- Scanning only in production / on a schedule. By then it's an incident. Scan on the PR and fail the build on high-severity findings.
- Treating SLSA as pass/fail. It's a ladder. Not knowing your level is the real failure, measure, then climb one rung.
Takeaways
Supply-chain security in six lines
- Most of your production code is other people's, the supply chain is the attack surface.
- Shift left: catch vulnerable dependencies on the PR, not in a post-incident pen-test.
- SBOM = an ingredients label per build, so 'are we affected?' is a grep.
- Sign artifacts (cosign/Sigstore) AND enforce verification at deploy, both halves or neither.
- Provenance proves how an artifact was built; SLSA grades how trustworthy that build is.
- Pin by hash, scope CI tokens tightly, scan every PR, your pipeline is production.
Where to go next
Supply-chain security lives inside your pipeline and is one expression of a broader 'trust nothing by default' mindset:
- CI/CD Fundamentals: What a Pipeline Actually Does, the pipeline these controls plug into.
- Zero-Trust Networking for Beginners, the same 'verify everything' principle, applied to the network.
- Hands-on CI/CD lab, build a pipeline you can then harden with scanning and signing.
Want to go deeper?
This article covers concepts taught hands-on in the Cloud Engineer and DevOps career paths, with real terminal labs, production scenarios, and structured lessons.