Last week I wrote about building a zero-cost CI/CD security pipeline for pull requests. That post covered what happens when a developer opens a PR — SAST, secret detection, dependency scanning, IaC analysis, all scoped to changed files and aggregated into one report.
This post covers what happens after the code passes that pipeline: building, signing, and verifying the container image that ships to production. Same philosophy — all open source, no licensing costs, everything demonstrable in a real repo.
In March 2025, the tj-actions/changed-files GitHub Action was compromised. Attackers pushed malicious code to the repository and moved the version tags to point to it. Any workflow using tj-actions/changed-files@v35 — by tag, which is how most people reference Actions — automatically pulled and executed the attacker's code. CI pipelines became the attack vector.
This is not a theoretical concern. It happened, it affected thousands of repositories, and the root cause was mutable references — tags that can be silently redirected to different code.
Supply chain security is the set of decisions that make this class of attack either impossible or detectable before it causes damage.
The first decision is the most fundamental: stop using mutable tags.
In your Dockerfile, this means pinning base images to their SHA digest rather than a tag:
The tag rust:alpine can be updated by the image maintainer at any time. The SHA digest cannot change — it is cryptographically bound to that exact image content. If the image changes, the digest changes, and your build fails explicitly rather than silently pulling different code.
The same principle applies to GitHub Actions. actions/checkout@v4 is a mutable tag. The correct form is:
The version comment preserves human readability and — critically — allows Dependabot to recognize the version and open PRs when updates are available. You get immutability without manual maintenance overhead.
One pattern worth calling out: it is easy to pin your Dockerfile correctly and miss the Actions in your CI workflow entirely. Your security tooling is part of your supply chain. If aquasecurity/trivy-action@master is compromised, your vulnerability scanner becomes the attack vector. Pin your security tools with the same rigor you apply to your application dependencies.
Pinned references are only as good as your process for keeping them current. A SHA pinned to a six-month-old version of a dependency with known CVEs is not a security improvement.
Dependabot handles this automatically with two ecosystem entries in .github/dependabot.yml:
yaml
The github-actions ecosystem reads the version comments on your SHA-pinned Actions and opens PRs when new versions release. The PR shows exactly which action updated and from which SHA to which SHA. Review, merge, done. You stay current without manual tracking.
Pinning ensures you know what went into the build. Signing proves what came out of it.
Cosign with keyless signing uses OIDC to tie the signature to the GitHub Actions identity that produced it — no long-lived keys to manage, rotate, or accidentally expose:
The — yes flag confirms the keyless OIDC flow. The signature is stored in the container registry alongside the image and can be verified by anyone with access to the registry:
A Software Bill of Materials documents every dependency in your image. For compliance purposes — FedRAMP, HIPAA, Executive Order 14028 — having an SBOM available is increasingly a requirement rather than a nice-to-have.
Syft generates the SBOM in SPDX format:
The critical step is attestation rather than just attachment. cosign attach sbom uploads the SBOM as a blob with no cryptographic binding — anyone could swap it out. cosign attest signs it with the same keyless OIDC identity and ties it to the specific image digest:
The difference matters for compliance: an attached SBOM tells you what was there at some point. An attested SBOM proves what was in this specific image at the time it was built, signed by the build identity that produced it.
For healthcare organizations specifically, this is no longer just good practice. The proposed HIPAA Security Rule NPRM published in January 2025, currently targeted for finalization in May 2026, explicitly requires a technology asset inventory and annual verification of business associate security practices.
An attested SBOM tied cryptographically to a specific image digest is a concrete, auditable answer to both requirements. The organizations that build these practices now won't be scrambling when the final rule drops. The 240-day compliance window after finalization moves faster than most security programs.
The final gate before deployment is a Trivy vulnerability scan against the built image. Running this after the build catches vulnerabilities introduced by the build process itself, not just the source dependencies:
exit-code: Ƈ' fails the build on critical or high findings. The pipeline does not deploy a vulnerable image.
Every one of these steps is implemented in my mhumble.io repo. The pipeline runs on push to main: build the image, sign it with Cosign, generate the SBOM with Syft, attest the SBOM, scan with Trivy, deploy only if the scan passes.
Total cost: $0. The tools are open source. The signing infrastructure uses GitHub's OIDC tokens. The registry is GitHub Container Registry on a public repo.
Supply chain security is not a product you purchase. It is a set of decisions applied consistently at every layer — from the base image tag in your Dockerfile to the Action SHA in your CI workflow. Your repo either demonstrates those decisions or it doesn't.
The code is available if you want to use it as a reference.
This is part two of a series on building AppSec tooling without enterprise licensing. Part one covered CI/CD security scanning on every pull request. Part three covers writing custom Semgrep rules for your internal patterns.
Originally published at https://www.linkedin.com.