In March 2025 a popular GitHub Action started printing CI secrets into build logs. Six months later a self-replicating worm was publishing poisoned npm packages with stolen developer credentials. By May 2026 an attacker had poisoned a build cache from a fork and minted a publish token inside a project's own release workflow. None of it needed a bug in application code; it went after the system that builds and ships it. This is the playbook: the threat model, a prioritised 30-day checklist, what to block versus warn, a tooling map, and how to measure progress.
Key takeaways
- The pipeline is the target. Every incident in this post went after developer or CI credentials, or a pipeline's right to publish; none needed a flaw in application code.
- Identity before scanners. Short-lived OIDC credentials and read-only default tokens take away the long-lived secrets most of these attacks were built to steal.
- SCA is necessary, not sufficient. It matches known advisories, and a malicious release published an hour ago has none yet. Cooldowns, lockfiles, SHA pins and provenance close the gap.
- Warn first, block few. Leaked secrets, unpinned third-party actions and unsigned images at production admission earn a block; most findings only warn.
What is DevSecOps supply chain security?
DevSecOps supply chain security is protecting everything that goes into a release, not only the code your team writes: source repositories, open-source dependencies, CI/CD workflows and third-party actions, the artifacts a build produces, and the credentials that publish and deploy them. In a DevSecOps pipeline those protections run as automated checks and policies on every change, not as a review before release.
Software composition analysis (SCA), the scanner most teams already run, covers one slice of that: known vulnerabilities in the packages you ship. For the basics (shift left, SAST, SCA, SBOMs), start with what DevSecOps is. This post goes after the pipeline itself, which NIST SP 800-204D treats as the place supply chain controls belong: artifacts, attestations, SBOMs and provenance built into CI/CD rather than bolted on beside it.
The 2025–2026 attacks that rewrote the threat model
The incidents below share a shape. Each one went after developer or CI credentials, or abused a pipeline's right to publish. Each is sourced to the maintainer's own advisory or postmortem, or to a government alert.
| When | Incident | How it got in | What it took | Control that limits it |
|---|---|---|---|---|
| Mar 2025 | tj-actions/changed-files (CVE-2025-30066) | Version tags moved to a malicious commit, potentially enabled by the compromised reviewdog/action-setup (CVE-2025-30154) | Secrets printed to workflow logs: access keys, GitHub and npm tokens, private RSA keys | SHA pins; no long-lived CI secrets |
| Aug 2025 | Nx “s1ngularity” | Shell injection in a pull_request_target workflow led to the npm publish token | Malicious versions that hunted for secrets, tried local AI tools and posted results to public GitHub repos | No privileged triggers on untrusted input; trusted publishing |
| Sep 2025 | Shai-Hulud worm | Compromised developer accounts | Over 500 packages; GitHub tokens and cloud keys stolen, and the developer’s npm access reused to publish more infected versions | Phishing-resistant MFA; known-good pins; rotation |
| Mar 2026 | Trivy, trivy-action, setup-trivy (CVE-2026-33634) | Compromised credentials, possibly kept through a non-atomic rotation; 76 of 77 trivy-action tags force-pushed | Runner memory scraped for secrets; SSH keys, cloud and Kubernetes credentials | SHA pins; treat reachable secrets as exposed |
| May 2026 | TanStack: 84 versions across 42 packages | A fork build under pull_request_target poisoned the pnpm cache the release job restored | An OIDC publish token minted inside the trusted release workflow | No caches in release jobs; no privileged fork builds |
| May 2026 | Nx Console and “Megalodon” (CVE-2026-48027) | A poisoned VS Code extension delivered by auto-update; malicious workflows injected into public repos | Internal GitHub repositories, CI/CD secrets, cloud credentials | Version pins; a wait before new releases; workflow review |
| Aug 2026 | ChainDrop (a Mini Shai-Hulud variant) | Stolen maintainer credentials, then npm tokens and GitHub Actions OIDC publishing | More than 400 npm packages across unrelated publishers | Protected publish environments; release-age minimums; known-good pins |
Sources: GitHub's tj-actions advisory (over 23,000 repositories impacted) and CISA's alert; the Nx postmortem; CISA on Shai-Hulud; the Trivy advisory; the TanStack postmortem; CISA on Nx Console and Megalodon; and Microsoft on ChainDrop. Our April 2026 recap covers the SAP wave of Mini Shai-Hulud.
Three lessons stand out. First, security tooling is part of the supply chain: the Trivy compromise shipped an infostealer through a vulnerability scanner's own GitHub Action. Second, provenance only helps if something checks it. Nx spotted its malicious versions because they lacked npm provenance, but, in its own words, “provenance doesn’t block unsigned packages from being installed”. Third, provenance is not trust. TanStack's malicious versions went out on a legitimately minted OIDC token because the attacker was already executing inside the release workflow. Its postmortem puts it plainly: “OIDC trusted-publisher binding has no per-publish review.” Microsoft reached the same conclusion about ChainDrop: packages republished through a hijacked workflow “can carry valid provenance because the publication originates from a legitimate workflow identity.”
The threat model: five places a pipeline gets poisoned
Map every control to one of five layers and the gaps become obvious. Risk names are from the OWASP Top 10 CI/CD Security Risks; controls from SLSA and GitHub's secure-use reference.
| Layer | What goes wrong | OWASP CI/CD risk | Reference control |
|---|---|---|---|
| Source | Unreviewed changes to code or to the workflow files themselves; privileged triggers running fork code | CICD-SEC-1 flow control; CICD-SEC-4 poisoned pipeline execution | Branch protection, required review, CODEOWNERS on .github/; SLSA Source track |
| Dependencies | Moved tags, a malicious new release, a typosquat, a compromised scanner or plugin | CICD-SEC-3 dependency chain abuse; CICD-SEC-8 third-party services | Lockfiles, SHA-pinned actions, release-age cooldowns, SCA |
| Build | Poisoned caches, shared runners, secrets readable by every step | CICD-SEC-4; CICD-SEC-7 insecure system configuration | Ephemeral runners, no caches in release jobs, SLSA Build L3 isolation |
| Artifacts | Nobody checks what was built, by whom, from which commit | CICD-SEC-9 improper artifact integrity validation | SBOM per build, signatures, provenance verified at deploy |
| Deploy identity | Long-lived cloud keys and publish tokens; write-everything job tokens | CICD-SEC-2 identity and access; CICD-SEC-5 pipeline access controls; CICD-SEC-6 credential hygiene | OIDC federation, read-only default GITHUB_TOKEN, environment protection |
SLSA v1.2 (24 November 2025) is backward compatible with v1.1 and adds a Source track whose top level requires two trusted people to review every change to protected branches. On the Build track, Level 2 means signed provenance from a hosted build platform; Level 3 stops one build run influencing another and keeps the provenance signing secrets away from user-defined build steps. TanStack's cache poisoning was exactly that kind of cross-run influence.
Deploy identity is where GitOps earns its place in a security conversation: with pull-based delivery, the cluster fetches reviewed changes from Git, so CI never needs cluster credentials at all.
A prioritised 30-day checklist
Order matters more than completeness: remove the most-stolen thing first (long-lived credentials), then the most-abused entry point (third-party code running with privileges), and only then add the evidence layer. Each week should end with changes merged, not a report written.
Week 1: inventory and identity
- Take a baseline. List every workflow, third-party action, runner and stored secret. Run OpenSSF Scorecard (its checks include Dangerous-Workflow, Token-Permissions and Pinned-Dependencies, each scored 0 to 10) and zizmor across your repositories.
- Make the job token read-only by default. GitHub's guidance is to set the
GITHUB_TOKENdefault to read access for repository contents and raise permissions per job. - Replace long-lived cloud keys with OIDC. With OpenID Connect, the cloud provider issues a token valid for a single job. Scope each cloud role to one repository and one environment.
- Turn on secret scanning for new commits and your Git history, and write down who rotates what when it fires.
Week 2: close the entry points
- Pin every third-party action to a full commit SHA. GitHub calls this “currently the only way to use an action as an immutable release”. Once repositories are pinned, enable the allowed-actions policy that fails unpinned workflows, and keep its
!blocklist ready for the next incident. - Remove privileged triggers from untrusted code. A
pull_request_targetworkflow must never check out or build fork code. Pass untrusted input such as pull request titles through environment variables, not inline${{ }}expressions in scripts. - Split build from release. No caches in release workflows, and
id-token: writeonly on the job that publishes. - Protect the workflow files. CODEOWNERS on
.github/and required review, so pipeline changes get at least the scrutiny product changes do.
# workflow default: read-only token
permissions:
contents: read
jobs:
publish:
environment: release
permissions:
contents: read
id-token: write # OIDC only where it is needed
steps:
- uses: actions/checkout@<full-40-character-commit-sha> # vX.Y.Z
Week 3: dependencies and evidence
- Install from lockfiles and add a cooldown. CISA's May 2026 alert advises waiting at least three hours before pulling a new package; since 14 July 2026 Dependabot waits three days by default before proposing a version update, while security updates still open immediately.
- Run SCA on every pull request, warn-only, against a baseline so existing debt goes to the backlog instead of blocking new work.
- Generate an SBOM on every build and store it next to the artifact. SPDX 3.0 (April 2024) added profiles for security and build information; CycloneDX 1.7 (October 2025) followed 1.6, which was ratified as ECMA-424. Pick the format your customers ask for.
- Publish packages with trusted publishing. npm trusted publishing uses OIDC instead of long-lived tokens and generates provenance automatically from GitHub Actions and GitLab; npm recommends restricting token-based publishing once it is configured.
Week 4: sign, verify, enforce
- Sign what you build. Cosign keyless signing binds a short-lived certificate to the workflow's OIDC identity and records it in the Rekor transparency log. GitHub artifact attestations give SLSA Build Level 2 on their own, and Level 3 with reusable workflows.
- Verify at admission, including who signed. A Kyverno ImageValidatingPolicy checks signatures and attestations; cosign verify takes the expected signer identity and OIDC issuer. Warn in development, enforce in production.
- Write the gate policy down: what blocks, what warns, who grants exceptions, for how long, and where they are recorded.
- Run a rotation drill. Pick one pipeline, assume every secret it can reach is exposed, as the Trivy advisory told its users to, and time how long revoking and reissuing all of them takes. Revoke them together: the Trivy maintainers' own rotation “was not atomic”, and their advisory says the gap could have let the attacker keep access.
This is the same order our DevSecOps consulting work follows: a read-only first week, then identity, then supply chain, then gates, delivered as pull requests your team reviews and owns.
What to block and what to warn
A gate that blocks on everything gets bypassed; one that blocks on nothing is a dashboard. Every check starts warn-only, and only high-confidence classes become blocking once they are accurate.
| Finding | Rollout | Once accurate | Why |
|---|---|---|---|
| Secret in a commit or a log | Alert from day one | Blocks, then rotate | High confidence; every minute it stays live is exposure |
| Third-party action not pinned to a SHA | Warn in the Scorecard and zizmor report | Blocks through the allowed-actions policy | Tags move; tj-actions and trivy-action were both retargeted |
| Privileged trigger on fork code, or script injection | Warn on workflow changes | Blocks changes to workflow files | The entry point for Nx and TanStack |
| Critical CVE with a fix available | Warn in the pull request | Blocks new introductions | Existing debt goes to the baseline and the backlog, not the merge queue |
| Dependency newer than the cooldown | Hold version updates | Stays a hold; security fixes exempt | New releases are when malicious versions do their damage |
| Unsigned image or missing provenance | Warn in development | Blocks at admission in production | Teams fix the pipeline, not the policy |
| SAST medium and low, licence notices | Warn in the pull request | Stays a warning | Useful context; too noisy to stop a release |
Every blocking rule ships with an exception path: who can grant one, for how long, and where it is recorded. That record is also what an auditor asks for.
Where SCA fits in DevSecOps, and where it stops
SCA belongs in every pull request: it finds the known-vulnerable library five levels down your dependency tree. In supply chain terms it has three blind spots, each needing a different control.
- New malicious releases. SCA matches components against advisories, and a version published an hour ago has none yet, exactly when a worm spreads. Lockfiles and release-age cooldowns cover this.
- Dependencies outside the manifest. Actions, build plugins and scanner binaries are pipeline dependencies, so they rarely appear in what SCA reads. GitHub's docs also note that Dependabot does not alert on SHA-pinned actions; zizmor's
known-vulnerable-actionsaudit fills that gap. - Integrity. SCA tells you what is in a package, not who built it or whether the build was tampered with. Signatures and provenance, verified before deploy, cover that.
AI-assisted coding widens the first gap by adding dependencies nobody chose deliberately; see vibe coding’s security and tech-debt bill.
The tooling map
Most teams already own more of this than they use. Start with the platform's built-in features; add open-source tools where there is a gap.
| Control | Open source | Built into the platform |
|---|---|---|
| Workflow linting | zizmor, OpenSSF Scorecard | GitHub allowed-actions policy: SHA pinning, blocklist |
| Secrets | gitleaks | GitHub secret scanning |
| SCA | Trivy, Grype | Dependabot alerts and cooldowns |
| SBOM | Syft, Trivy (SPDX or CycloneDX) | Stored with the release, or attached to the image as an attestation |
| Signing and provenance | Sigstore cosign, SLSA provenance | GitHub artifact attestations; npm trusted publishing |
| Deploy-time verification | Kyverno, Gatekeeper with Ratify | gh attestation verify for GitHub attestations |
| Pipeline identity | Vault, External Secrets for what remains | OIDC federation to AWS, Azure and GCP |
| Infrastructure policy | OPA and Conftest on Terraform plans | Cloud-native posture services |
How to measure progress
Supply chain work is easy to declare and hard to verify. Each measure below is countable per repository, so a spreadsheet is enough to start.
- Long-lived cloud credentials stored as CI secrets. Target: zero.
- Workflows with a read-only default token. Target: all.
- Third-party action references pinned to a full SHA. Target: all, then enforced by policy so it cannot regress.
- Release artifacts shipped with an SBOM and signed provenance. Target: all.
- Production deploys verified at admission, signer identity included. Target: all.
- Time to rotate every secret one pipeline can reach, measured in a drill: the number that matters on the morning of the next advisory.
- Open exceptions and their age. An exception older than its expiry is a finding.
- Scorecard score per repository, over time. Watch the trend, not the absolute number.
Our take
The pattern across eighteen months of incidents is not subtle. Attackers stopped hunting for bugs in your product and started hunting for the pipeline's credentials, because one stolen publish credential can push malicious releases of every package its owner maintains. The defence is not another scanner. It is fewer secrets, immutable references, privileged steps kept away from untrusted code, and evidence someone actually verifies.
Start with identity this week; it removes the most risk per hour of effort. If you want the controls designed and built alongside your team, that is what our DevSecOps consulting does, and if deploy credentials still live in CI, GitOps consulting takes them out.
Related: DevSecOps consulting · What is DevSecOps? · GitOps consulting · Vibe coding’s security bill · AI broke production: April 2026