Selected for GitHub's Secure Open Source Fund. See how it's shaping the future of AI agent security.

Learn more

Blogs

How We Hardened Nasiko's Software Supply Chain

Nasiko

Cover card for “How We Hardened Nasiko's Software Supply Chain”: the Nasiko wordmark above the GitHub Secure Open Source Fund logo, with gold and green pixel-cube artwork.

The follow-up we promised: what the GitHub Secure Open Source Fund changed about how we build Nasiko, and what it taught us to see in an architectural choice we had already made.

In this post, we argued that the ordinary security foundations mattered as much as the agent-specific work, and we promised to come back to the supply chain in more detail. This is that follow-up.

Here is the short version, which took us three weeks and a lot of other people's hard-won experience to state plainly. The source code people inspect is only one part of what they ultimately trust. The build workflows that turn it into an artifact, the dependencies pulled into that artifact, and the accounts allowed to change either one can alter what users actually run without changing the code they thought they reviewed. Nasiko exists to help teams govern and secure agents in production, so holding our own build and release path to that standard was not optional.

So we spent the program's supply-chain time on three surfaces: how we build, what we ship, and who can push.

The source code is only one attack surface

Diagram: what users trust. Commits, source code, accounts, and dependencies all feed the CI pipeline that produces the shipped binary users actually run.
The shipped binary is what users run

Two incidents from the last couple of years make the point better than we could.

In March 2025, a widely used GitHub Action, tj-actions/changed-files, was compromised. The attackers repointed the Action's version tags to a malicious commit, and every workflow that referenced those moving tags began running code that dumped CI secrets into build logs. More than twenty thousand repositories referenced the Action. The ones that had pinned it to a specific commit hash, rather than a mutable tag, largely ran the version they had already reviewed and were spared. The incident was later tracked as CVE-2025-30066.

Later in 2025, the npm ecosystem was hit by a self-replicating worm known as Shai-Hulud. It spread through compromised maintainer accounts and publishing tokens, planting malicious install scripts in packages, harvesting additional credentials from the machines that installed them, and publishing itself further. It reached hundreds of packages in a first wave and considerably more in a second.

Neither attack was due to a bug in anyone's source code. One turned on a moved tag in a build step. The other turned on compromised publishing credentials and a script that ran at install time.

Both then rode the trust that a normal CI pipeline and a normal dependency graph extend by default. That trust is the surface we set out to shrink.

How we build: pin the pipeline, scope the token

The tj-actions pattern comes down to mutability. A tag like @v4 is mutable: a maintainer, or an attacker with the necessary access, can move it to point at another commit. A full-length commit SHA identifies a specific commit and cannot simply be repointed to different code. So we pinned our third-party GitHub Actions to full commit SHAs rather than tags, which means a workflow runs the code we reviewed even if an upstream tag is later moved.

That raises the obvious question: what about security updates? Pinning does not mean choosing between safety and staying current. Dependabot can keep SHA-pinned Actions up to date, bumping both the pinned commit and the version recorded in the trailing comment, so the pins move forward in review rather than going stale.

We also scoped GITHUB_TOKEN permissions to the principle of least privilege. Rather than relying on repository or organization defaults, each workflow gets only the permissions it actually needs, so a compromised step reaches less.

In practice, both changes are a couple of lines at the top of a workflow:

jobs:
  build:
    permissions:
      contents: read   # least privilege, not the inherited default
    steps:
      - uses: actions/checkout@<full-commit-sha>
        # pinned to a commit, with the version in a comment

Neither change is glamorous, and that is the point. Both would have blunted a real, recent attack at its point of entry.

What we ship: the package graph we chose not to have

Diagram: a sprawling package-manager-resolved dependency tree with install-script areas, next to a short list of vendored, reviewed single-file ES modules.
Reviewed. Pinned. Intentional.

Nasiko's web UI has no JavaScript build step and no package-manager resolution step. It is vanilla JavaScript web components: native ES modules and CSS, embedded into the server binary and served from the same origin. The third-party libraries the UI uses are vendored, each committed to the repository as a single, reviewed ES module file and replaced wholesale on upgrade, because there is no package manager resolving anything at build or run time.

We did not originally adopt that design as a security measure. But the program's supply-chain lens made plain what it buys us. An attack like Shai-Hulud propagates through install-time scripts and transitive dependencies, the machinery a package manager runs on your behalf when it resolves a tree of packages you never directly chose. A UI with no package manager in its build or runtime path has almost none of that machinery to exploit: no install scripts, no transitive graph resolving to versions published minutes ago, and every third-party byte sitting in the repository as a reviewable file in a pull request rather than a line in a lockfile.

This is not a claim that Nasiko has no dependencies. Our backend has its own module graph and its own supply-chain discipline. The point is narrower, and we think it's more useful. On the UI surface, where we could avoid a package-manager-resolved dependency tree entirely, we did. Monitoring a dependency graph is useful. Not needing that dependency graph in the first place is a different kind of security control.

Who can push: the accounts are the real entry point

It is worth noting what both incidents above had in common. Both exposed the same uncomfortable fact: identities and credentials are part of the software supply chain. The tj-actions chain used stolen, write-capable credentials to alter trusted Actions and their tags. Shai-Hulud spread through compromised maintainer accounts and publishing tokens. The memorable part of a supply-chain attack is the payload, but the leverage almost always comes from an account.

So we treated maintainer access as part of the supply chain rather than as something separate from it. We confirmed that multi-factor authentication is enforced for all maintainers and major contributors, and we audited access and removed unnecessary permissions. Fewer standing privileges means a single compromised account reaches less.

We also switched on GitHub's repository security features at no cost and left them on. We enabled secret scanning to detect supported credentials committed to the repository, code scanning, Dependabot for the dependencies we do carry, branch protections that require review before a merge, and private vulnerability reporting so a researcher can reach us without opening a public issue. None of these are novel. All of them were worth enabling and configuring.

This is a set of trade-offs, not a free win

We would rather be honest about the costs than oversell the posture.

Vendoring third-party code means we own its upgrades. No bot quietly bumps a vendored ES module for us, so we have to watch upstream and replace files deliberately, and a patch we miss is a patch we miss. Pinning Actions to SHAs has a related shape: the pin is safe, but it is also frozen, so we lean on Dependabot and attention to move pins forward rather than letting them go stale. And a UI with no package manager gives up real conveniences that a framework and a live ecosystem provide.

These are the right trade-offs for infrastructure that other teams run in production. They are not the right trade-offs for every project, and we would not pretend otherwise.

A supply-chain checklist you can run this week

If you want to make this class of attack harder against your own project, roughly in order of effort to payoff:

  • Pin third-party GitHub Actions to full commit SHAs rather than tags, and let Dependabot keep the pins up to date.
  • Scope each workflow's GITHUB_TOKEN to the permissions it actually needs, rather than inheriting the default.
  • Turn on the free repository features: secret scanning (with push protection if you can), code scanning, Dependabot, branch protections that require review, and private vulnerability reporting.
  • Enforce multi-factor authentication for everyone with write access, and prune unused permissions.
  • Look closely at your runtime package graph and reduce what you carry rather than just monitoring it.

The first four are operational controls. The fifth is an architectural question, and potentially the more consequential one.

Thanks again to GitHub, the GitHub Security Lab, and our Secure Open Source Fund cohort. The incidents above were not abstract examples. They were recent reminders of how quickly trust can carry an attack, and much of this post is other maintainers' experience passed along. If you harden one workflow file after reading it, it is worth writing.

Read the full announcement on GitHub: https://github.blog/open-source/maintainers/what-50-open-source-projects-taught-us-about-security-in-the-ai-era/

Every agent.
Accounted for.