Open Source Security

npm Team Access Model Hardening

Npm's team-based permissions are more expressive than most organizations use. A walkthrough of the access model and the configurations that actually reduce blast radius.

Shadab Khan
Security Engineer
7 min read

The npm organization access model has been in its current shape since the relaunch in 2016 and has gained granular access tokens, 2FA enforcement, and audit logs in the years since. In aggregate it is a reasonably capable access control system. In practice, most organizations I audit use a single team called "developers" with every employee added and every package accessible by every team member. That configuration is a single-point-of-compromise waiting to happen.

This post walks through the model as it actually works today, the configurations that reduce blast radius, and the migration path from the naive "everyone has everything" state most orgs find themselves in.

The Model, Briefly

An npm organization contains packages and users. Users are grouped into teams. Teams have access to specific packages at either read-only or read-write permission. A user's effective permissions are the union of the permissions of all teams they belong to.

Organizations also have owners, who can modify org settings, add and remove members, and change billing. Owners bypass team-level permissions; they can access any package in the org.

The access model has one major gap that comes up in audits: there is no concept of "can publish but not change access." If a user is on a team with read-write access to a package, they can both publish new versions and modify the team permissions on that package. This is a coarser grain than many teams would want.

The "Everyone Has Everything" Problem

The default configuration when you create an npm organization puts every member on a single "developers" team and gives that team read-write on every package. This is operationally convenient and security-painful.

The concrete risk: if any single developer account is compromised, the attacker can publish to every package in the organization. For an org with a hundred developers and thirty packages, that is a factor of one hundred amplification of account-takeover impact.

I migrated a client off this model in November 2023. They had 147 developers across four engineering groups (web, mobile, infra, data) and 52 npm packages. The migration took about six weeks of planning and two weeks of execution.

Mapping Teams To Packages

The mapping I pushed them toward:

  • One team per package, named pkg-<packagename>, with read-write. This is the "owner team" for that package.
  • One team per engineering group, with read-only on every package. This lets any developer install any internal package without being able to publish to it.
  • A publishers team, specific per package or per product, with read-write. Membership is explicit and minimal; three to five people per package.

Owners of the org are limited to two people, both on the security team. Developers who need to publish are added to specific publisher teams, not given org-owner status.

This configuration reduced the number of users who could publish any specific package from 147 to an average of 4. The developers who publish often are not materially inconvenienced; the developers who publish rarely go through a named owner, which adds an hour of latency to rare publishes but is a good trade.

The Team Size Principle

In the model I just described, the critical number is the size of the team with write access to a given package. The goal is the smallest number of humans who can realistically operate the package. For most internal packages, that number is three to five: two primary maintainers and two or three backups.

Going smaller creates a bus-factor problem. Going larger increases the compromise surface.

For high-impact public packages (top of downloads, widely consumed), I have seen orgs go all the way down to two humans and a trusted CI identity (an OIDC trusted publisher), with the humans acting only in break-glass scenarios. That is the right answer for a package like a popular logging library; it is overkill for an internal tool used by twelve developers.

The 2FA Requirement

Since 2022, npm has required 2FA for publishing to packages above a download threshold ("high-impact" packages). Inside an organization, you can require 2FA for all members via the org settings. Do this. Every org I audit that has not enabled it has a measurable percentage of accounts without 2FA, and those accounts are the ones an attacker targets first.

The 2FA enforcement cascades: once enabled, members without 2FA lose their ability to publish. A few will complain that setting up an authenticator app is inconvenient. The correct response is to help them set it up, not to disable the requirement.

Be aware of one gotcha: tokens created before 2FA enforcement continue to work for some operations. Rotate all tokens after enabling 2FA to ensure the new requirement applies to every active credential.

Audit Log Usage

Npm organizations have an audit log available to org owners. It records package publishes, team membership changes, permission changes, and token creations. The log is retrievable via the web UI and via a partially-documented API endpoint (https://registry.npmjs.org/-/org/<org>/audit-logs).

The log is not alerted on by default. I ship the log to a SIEM via a small cron job that polls the API hourly and writes new entries to Splunk. Alerting rules that have caught real issues:

  • A permission change outside business hours in the owning team's timezone.
  • A new token created by an account that rarely creates tokens.
  • A publish to a package that hasn't been touched in over 180 days.
  • An org owner addition. (This should be a rare event and always trigger a page.)

In October 2023 one of these rules caught an attempted takeover of a developer account. The attacker created a new token and then attempted to publish to a dormant package. The alert fired, we revoked the token within fifteen minutes, and the publish never completed. The developer's account had been accessed through a session cookie lifted by infostealer malware on their home machine.

Team Rename And Delete Hygiene

Npm allows team renames and deletes via the CLI (npm team) or the web UI. Neither emits a visible "team was deleted" notification to members. If an attacker with owner access deletes a team, the membership is gone; recreating the team does not restore membership automatically.

My practice: the audit log feeds a nightly backup of team-and-membership state to a separate Git repo. If a team is deleted, we can restore its composition without reconstructing who was on it from memory.

The Break-Glass Account

Every org should have a break-glass owner account that is used only during incidents. Credentials for that account are stored in a sealed envelope in a physical safe; digital access requires approval from two security officers. This is the account used when the regular owners are locked out or compromised.

I have used this twice in five years. It is worth the operational overhead.

How Safeguard Helps

Safeguard connects to npm's audit log API and provides continuous monitoring of team membership changes, permission changes, and publishes, with the alerting rules mentioned above pre-built. We visualize team-to-package access so you can see, for each package, exactly which humans can publish and flag outliers (the developer who can publish 47 packages nobody else touches, for example). Our policy engine enforces minimums like "every team must require 2FA" and "no single developer has write access to more than N packages" as continuous checks rather than one-time audits. If your org is migrating from "everyone has everything" to a least-privilege model, Safeguard produces the mapping and tracks progress week by week.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.