New Dec 4, 2025

No more tokens! Locking down npm Publish Workflows

Top Front-end Bloggers All from Zach Leatherman View No more tokens! Locking down npm Publish Workflows on zachleat.com

With the recent spate of high profile npm security incidents involving compromised deployment workflows, I decided that it would be prudent to do a full inventory of my npm security footprint (especially for 11ty).

Just in the last few months:

Expand to see the insecure YAML from the S1ngularity attack
# Some content omitted for brevity
on:
  pull_request:
    types: [opened, edited, synchronize, reopened]
  # …
jobs:
  validate-pr-title:
    # …
    steps:
      # …
      - name: Create PR message file
        run: |
          mkdir -p /tmp
          cat > /tmp/pr-message.txt << 'EOF'
          ${{ github.event.pull_request.title }}

${{ github.event.pull_request.body }} EOF

- name: Validate PR title run: | echo "Validating PR title: ${{ github.event.pull_request.title }}" node ./scripts/commit-lint.js /tmp/pr-message.txt

Given the attack vectors of recent incidents, any packages using GitHub Actions (or other CI) to publish should be considered to have an elevated risk (and this was very common across 11ty’s numerous packages).

I’ve been pretty cautious about npm tokens. I have each repository set up (painstakingly) to use extremely granular tokens (access to publish one package and one package only). This limits the blast radius of any compromise to a single package and has helped manage my blood pressure (I accidentally leaked a token earlier this year).

Security Checklist

I’ve completed my review and made a bunch of changes to improve my security footprint on GitHub and npm, noted below. The suggestions below avoid introducing additional third-party tooling that may decrease your footprint short-term (while actually increasing it long-term).

Caveat: my current workflow uses GitHub Releases to trigger a GitHub Action workflow to publish packages to npm (and this advice may vary a bit if you’re using different tools like GitLab or pnpm or yarn, sorry).

  1. Use Two-Factor Authentication (2FA) for both GitHub AND npm, for every person that has access to publish. This is table-stakes. No compromises. Require 2FA everywhere.
    • On GitHub, go to your organization’s Settings page and navigate to Authentication Security. Check the Require Two-factor authentication for everyone and Only allow secure two-factor methods checkboxes.
    • npm requires you to specify this on a per-package basis that I describe in the Restrict Publishing Access section below.
  2. When logging into npm and GitHub, use your password manager exclusively! Never type in a password or a 2FA code manually. Your password manager will help ensure that you don’t put in your credentials on a compromised (but realistic looking) domain.
    • Would you know that npmjs.help was a spoofed domain? Maybe on your average day, but on your worst day? When you didn’t sleep well the night before? 😴
  3. Review GitHub users that have the Write role in your repositories (Write can create releases).
  4. Find any repositories using NPM tokens and delete the tokens in the settings for both GitHub and npm. We’re moving to a post-token world.
  5. Switch to use Trusted Publishers (OIDC) in the Settings tab for each npm package. This will also setup the release to include provenance as well (which is great).
    • This scopes your credentials to one specific GitHub Action (you specify which file to point to in .github/workflows/) and allows you to remove any references to tokens in the GitHub Actions YAML configuration file.
    • The big goal here for me was to completely separate my publish workflow and credentials and disallow any access to those credentials from other workflows in the repository (usually unit tests that run on every commit to the repo). You could also use GitHub Environments to achieve this. This limits the blast radius from worm propagation (via postinstall or preinstall) to publish events only (not every commit), which is far more infrequent.
  6. Restrict npm Publishing Access in the Settings tab for each npm package. Use the Require two-factor authentication and disallow tokens (recommended) option. Death to tokens!
  7. Check in your lock file (e.g. package-lock.json for npm). This is especially important when using a release script that uses npm packages to generate release artifacts. Use npm ci instead of npm install in your release script.
  8. GitHub Actions configuration files should pin the full SHA for uses dependencies (e.g. eleventy-plugin-vite). I learned that Dependabot can update and manage these too!

Other good ideas

Given the above changes, I would consider the following items to not to be of immediate urgency (though still recommended).

Stay safe out there, y’all!

Additional Reading

Scroll to top