Supply chain is now the most common known way into the breaches we track, and almost every case runs on a stolen or misused machine identity: a publishing token, a bot account, a CI secret or a build signing key. In The State of NHI & AI Agent Breach Report 2026, NHI Mgmt Group counted 51 of the 200+ breaches in its database that came through a poisoned package, build tool or CI pipeline. This guide reads those cases side by side to find the steps that repeat, then sets out the controls that break each step and the order to put them in. It is written for platform, AppSec and identity teams who own package publishing, CI/CD and the credentials that sit inside them. For the general programme see the Software Supply Chain Security Guide; for market-wide numbers see the Supply Chain Attack Statistics Guide.
Key takeaways
- The route is supply chain, the asset is an NHI. In the report, the most common pairing is supply chain with a CI/CD or publishing token (34 breaches), followed by a secret or password (25), a source control token (24) and a cloud credential (18).
- One publishing identity reaches every downstream user. Shai-Hulud and ChainDrop show a stolen npm token being used to republish every package its owner could touch, so one leak becomes hundreds of poisoned packages within hours.
- Provenance proves the pipeline, not the commit. ChainDrop and Tensorlake were published through trusted publishing with valid provenance, because the attacker pushed to the branch that feeds the release workflow.
- Leftover credentials keep attacks alive. Trivy (incomplete rotation), Ultralytics (a PyPI token kept after the move to trusted publishing) and Ledger (a former employee's npm access) all turned a contained incident into a second wave.
- The fix order is publish path, then CI secrets, then consumption. Remove stored publishing tokens, scope and shorten CI credentials, protect release branches and tags, then delay, pin and script-block what you install.
What the report data shows
All figures in this section are from NHI Mgmt Group, The State of NHI & AI Agent Breach Report 2026 (October 2026), which covers breaches and credential exposures from November 2017 to October 2026. A breach can have more than one route and more than one identity type, so the counts overlap.
| Measure | Figure in the report | What it means for defenders |
|---|---|---|
| Supply chain breaches | 51 of 200+ (23%), the #1 known route in | Publishing and build identities deserve the same protection as production admin |
| Supply chain with CI/CD or publishing token | 34 | Registry and pipeline tokens are the single most abused asset in this route |
| Supply chain with secret or password | 25 | Payloads harvest whatever secrets the install host can read |
| Supply chain with source control token | 24 | GitHub PATs and bot tokens are the step before the publish |
| Supply chain with cloud credential | 18 | CI runners and developer laptops hold cloud keys worth stealing |
| Supply chain share of 2026 breaches | 38% | The route is growing, not fading |
| AI-related breaches that came through supply chain | 13 | Agent skills, MCP servers and AI libraries are now part of the same route |
The attack chain that repeats
Across the cases below, the same five steps appear. Most incidents use four or five of them. Each step is a place to break the chain.
- Get a foothold on the publish path. A privileged workflow runs untrusted code (Trivy, the second Shai-Hulud wave at PostHog, Ultralytics), a maintainer account is taken over (ChainDrop, OVHcloud CLI) or an old credential is still valid (Ledger).
- Take a publishing or bot identity. A GitHub PAT, a bot account token, an npm or PyPI token, or simply write access to the branch that a trusted publishing workflow builds from.
- Ship through the trusted channel. A release, a moved Action tag or a replaced release asset reaches users who trust the name. Valid provenance does not help when the source itself is malicious.
- Harvest machine credentials at install or run time. A preinstall hook or poisoned Action reads tokens, cloud keys, SSH keys, Vault and Kubernetes tokens, and in 2026 also AI tool configuration files.
- Reuse what was stolen to spread. Stolen registry and source control tokens publish the victim's own packages, plant workflows or seed the next compromise in the chain.
Cases and the identity that failed
| Breach | Foothold | Identity abused | The control that would have broken the chain |
|---|---|---|---|
| Trivy Supply Chain Attack 2026 | A pull_request_target workflow ran fork code with repository privileges | A privileged GitHub PAT, the aqua-bot service account, an Open VSX token held by a former employee | No fork code in privileged workflows; atomic rotation of every reachable credential, then proof the old ones fail |
| Shai-Hulud npm Worm 2025 | At PostHog, a pull_request_target workflow ran attacker code and exposed a bot PAT | npm publishing tokens, GitHub PATs, Actions secrets, cloud credentials | Trusted publishing; single-package tokens; install scripts off on CI |
| ChainDrop npm Worm 2026 | Maintainer GitHub account taken over, push to main triggered the release workflow | Maintainer account, OIDC trusted publishing, stolen npm tokens and PATs | Required review on release jobs and protected main; minimum release age for consumers |
| tj-actions/changed-files Compromise 2025 | Bot PAT leaked through a compromised reviewdog Action in tj-actions' own CI | @tj-actions-bot PAT; runner secrets printed to public logs | Pin Actions to full commit SHAs; replace bot PATs with GitHub App tokens or OIDC |
| Ultralytics PyPI Compromise 2024 | Branch name injected into a shell step in a pull_request_target workflow | A long-lived PyPI token never revoked after the move to trusted publishing | Delete legacy tokens once trusted publishing works; pass untrusted values through environment variables |
| Ledger Connect Kit npm Compromise 2023 | A former employee was phished | npm access that was not revoked at departure, used via a session token or API key past 2FA | Offboarding that covers external registries; organisation-owned, pipeline-only publishing |
| OVHcloud CLI Compromise 2026 | A contributor's GitHub account, method not known | Release edit rights on the repository | Immutable releases; release rights treated as privileged access |
| SolarWinds Supply Chain Compromise | Compromised credentials into the development environment | Build and code-signing trust, SAML token-signing certificates, service principal credentials | Hardened, isolated builds; HSM-held signing keys; alerts on new service principal credentials |
The newer cases are faster versions of the old ones. SolarWinds needed months inside one build system. ChainDrop went from a poisoned commit at 09:02 UTC to 444 packages and 2,212 malicious versions by 13:20 UTC on 4 August 2026, according to StepSecurity. Tensorlake shows the same family returning in October 2026 with valid provenance; see the Tensorlake npm Worm 2026 page.
Lesson 1: remove stored publishing tokens
A long-lived registry token is a standing key to every user of a package. Trusted publishing replaces it with a short-lived token minted per workflow run through OpenID Connect.
- npm: configure a trusted publisher naming the exact workflow (GitHub Actions on GitHub-hosted runners, GitLab CI/CD on GitLab.com shared runners, or CircleCI cloud), then set the package's Publishing access to "Require two-factor authentication and disallow tokens". npm's documentation notes self-hosted runners are not yet supported. See the npm Trusted Publishing and Provenance Guide.
- PyPI: trusted publishing mints an API token that PyPI documents as valid for 15 minutes. After switching, delete every project-scoped and user-scoped token that could still upload, which is exactly what Ultralytics missed. See the PyPI API Tokens and Trusted Publishing Security Guide.
- Where a token is unavoidable: one package, the shortest expiry the registry allows, organisation-owned, stored only in the protected release environment. See the npm Tokens Security Guide.
- Offboarding: leavers lose registry, Open VSX, Marketplace and GitHub organisation rights the same day as SSO. Ledger and Trivy both had external publishing access outlive employment. See the NHI Offboarding and Decommissioning Guide.
Lesson 2: protect what feeds the publish path
Trusted publishing moves the target from the token to the branch, tag and workflow. ChainDrop's maintainer used OIDC for npm and one-time codes for GitHub, and the release still shipped with valid provenance.
- Privileged triggers: GitHub's secure use reference says to avoid
pull_request_targetwhen it is not necessary and that such workflows must not check out untrusted code. Preferworkflow_runfor privilege separation. - Script injection: pass untrusted context (branch names, titles) through an intermediate environment variable, never inline in a
run:step. - Release gates: run publish jobs in a protected environment with required reviewers, protect main and release tags, require signed commits, and alert on force pushes and tag deletions.
- Bot identities: replace bot PATs with GitHub App installation tokens or OIDC, set
GITHUB_TOKENto read-only by default and raise permissions per job. - Release assets: turn on immutable releases so assets cannot be swapped after the fact, as happened to OVHcloud CLI.
- Build isolation: keep release jobs from restoring caches that less-trusted jobs can write. SLSA v1.2 Build L3 asks for strong isolation between runs and signing secrets kept away from user-defined build steps.
See the GitHub and GitHub Actions Security Guide and the CI/CD Pipeline Identity Security Guide.
Lesson 3: make CI secrets worth less
The payloads in Trivy, tj-actions, Shai-Hulud and ChainDrop read runner memory and well-known file paths. Whatever a job can see, the attacker gets.
- Federate instead of storing: use OIDC to cloud providers and Vault for short-lived, well-scoped tokens per run. See the GitHub Actions OIDC Federation Hardening Guide.
- Scope per job: no cloud or publishing secrets in jobs that install third-party packages or run tests on external pull requests.
- One credential, one system: a CI credential that can publish every package or reach every account turns one leak into a campaign.
- Logs are a leak path: tj-actions printed secrets into public workflow logs. Restrict log visibility and scan logs for secrets after any incident.
Lesson 4: consume defensively
- Pin by digest: pin Actions to full-length commit SHAs from the action's own repository, not a fork. Tags were moved in Trivy (75 of 76 trivy-action tags per the breach page) and tj-actions.
- Delay new versions: set a minimum release age for dependency updates. In several of these cases the malicious versions were flagged and pulled within hours, Tensorlake within about 11 minutes of publication.
- Block install scripts: run
npm ci --ignore-scriptsin CI with an allow list for packages that need scripts. See the npm Install-Time Security Guide. - Verify, then trust: check hashes and attestations on downloaded binaries. A pinned SHA-256 in a customer's CI caught the OVHcloud swap, not the publisher.
- Keep secrets off developer disks: move cloud keys and tokens out of
.envfiles and tool configs into a secrets manager or short-lived credentials.
Decision guidance
| Situation | Choose | Trade-off |
|---|---|---|
| You publish open-source packages from GitHub-hosted runners | Trusted publishing plus "disallow tokens" | Branch and workflow protection become the security boundary |
| You publish from self-hosted runners | Single-package, short-expiry token in a protected environment, rotated on a schedule | A stored secret remains; monitor every publish against pipeline runs |
| Small team, few packages | Trusted publishing, required reviewer on release, SHA pinning via Dependabot or Renovate | Release reviews slow emergency fixes |
| Large organisation, hundreds of repositories | Central inventory of publishing identities, organisation-level Actions allow list, policy-as-code on workflows | Needs ownership for every package and bot |
Implementation in four phases
- Week 1 to 2, inventory: list every package you publish, every identity that can publish it (people, bots, tokens, trusted publishers) and every workflow using
pull_request_targetorworkflow_run. See the NHI Discovery Across Cloud, SaaS, CI/CD and On-Premises Environments. - Week 2 to 4, close the doors: fix privileged triggers, set
GITHUB_TOKENread-only by default, pin Actions to SHAs, remove former staff from every registry. Test changes on one repository first. - Month 2, change the publish path: enable trusted publishing per package, run one release through it, then disallow tokens and revoke the old ones. Roll back by re-enabling a single scoped token in the protected environment, never a personal one.
- Month 3, detect and rehearse: alert on publishes without a matching pipeline run, rehearse an atomic rotation of all CI and publishing credentials, and confirm old credentials fail. Use the Supply Chain Compromise Response Playbook.
Detection and measurement
- Publish without a pipeline run: compare registry publish events with release workflow runs; any mismatch is an incident.
- Tag and release changes: force pushes, tag moves and edits to existing release assets.
- New install scripts: a release that adds a
preinstallorpostinstallhook. - Runner egress: connections from runners to unexpected domains or new public repositories created by your accounts.
- Developer tool persistence: unexpected changes to
.vscode/tasks.jsonor.claude/settings.json, both used by ChainDrop.
Metrics that show progress:
- Percentage of published packages using trusted publishing with tokens disallowed.
- Number of long-lived registry and bot tokens remaining, and their maximum age.
- Percentage of third-party Actions pinned to a full commit SHA.
- Workflows using
pull_request_targetwith secret access (target: zero). - Time to revoke all publishing access for a leaver.
- Time from a rotation drill start to proof that every old credential fails.
Framework mapping
| Lesson | MITRE ATT&CK | NIST SP 800-53 Rev 5 | NIST CSF 2.0 | CIS Controls v8.1 |
|---|---|---|---|---|
| Poisoned packages and Actions | T1195.001, T1195.002 | SR-3, SR-4, SR-11, CM-14 | GV.SC-05, GV.SC-07 | 16 Application Software Security |
| Stolen publishing and bot tokens | T1528, T1550.001 | IA-5, AC-6 | PR.AA-01, PR.AA-05 | 5 Account Management, 6 Access Control Management |
| Secrets harvested from hosts | T1552.001, T1552.004 | IA-5, SC-12 | PR.AA-01 | 3 Data Protection |
| Hijacked maintainer and leaver accounts | T1078, T1586 | AC-2, IA-2 | PR.AA-03 | 5 Account Management |
| Build system integrity | T1195.002 | SA-10, SA-15 | DE.CM-09 | 16 Application Software Security |
ISO/IEC 27001:2022 Annex A controls 5.21 (managing information security in the ICT supply chain), 5.17 (authentication information) and 8.32 (change management) cover the same ground.
Common failure patterns
- Rotating most credentials: Trivy's first rotation was not comprehensive and residual access let the attacker return weeks later.
- Adding trusted publishing without removing tokens: the Ultralytics second wave used a token kept after the switch.
- Offboarding only the identity provider: Ledger's former employee still had npm rights.
- Treating provenance as proof of safety: ChainDrop and Tensorlake releases carried valid provenance.
- Pinning to tags: moved tags delivered stealers in Trivy and tj-actions.
- Revoking before cleaning: Shai-Hulud tried to wipe the home directory when it could not authenticate, and the ChainDrop and Tensorlake monitors can wipe when a watched token is revoked, so remove persistence first, then revoke from a clean machine. See the Leaked Credential Response Playbook.
Practitioner checklist
- Inventory every package, its publishing identities and their owners.
- Enable trusted publishing and disallow token publishing on every package that supports it.
- Delete every legacy registry token after the first successful trusted publish.
- Remove
pull_request_targetfrom workflows with secret access, or prove they never check out untrusted code. - Set
GITHUB_TOKENto read-only by default and replace bot PATs with GitHub App tokens or OIDC. - Pin third-party Actions to full commit SHAs and keep an organisation allow list.
- Require reviewers on release environments, protect release tags and turn on immutable releases.
- Block install scripts in CI and set a minimum release age for new dependency versions.
- Remove leavers from registries and marketplaces on the same day as SSO.
- Rehearse an all-at-once rotation and verify old credentials fail.
Standards and references
- NHI Mgmt Group: The State of NHI & AI Agent Breach Report 2026 (October 2026)
- npm Docs: Trusted publishing for npm packages
- PyPI Docs: Publishing with a Trusted Publisher
- GitHub Docs: Secure use reference for GitHub Actions
- SLSA: Build Track Basics (v1.2)
- MITRE ATT&CK: T1195 Supply Chain Compromise
- NIST: SP 800-53 Rev. 5 Security and Privacy Controls
Related NHI Mgmt Group resources: Software Supply Chain Security Guide · CI/CD Pipeline Compromise Playbook · Shai-Hulud npm Worm Guide · Non-Human Identity and Secrets Statistics Guide