Guides

Why a package install returns 403, and what to do

A 403 from npm, pip or dotnet inside CI usually means a policy said no. How to tell policy from authentication, read the refusal, and fix the right thing.

Updated

A build that installed fine yesterday fails with 403 Forbidden from the package manager. The message names a package and a version and nothing else. Before anyone rotates a token or reruns the pipeline, it is worth thirty seconds to work out which of two very different things happened.

Policy or authentication?

A registry answers 403 for two reasons that look identical to a build tool:

  • Authentication. The token is missing, expired, or scoped to the wrong organization. Every request fails, not just one package, and the failure starts the moment the token changed.
  • Policy. The registry understood the request, checked the version against the rules your organization configured, and refused to serve it. Only that package fails. Everything else in the same build resolves.

The quickest tell is breadth. One package failing while its neighbours install is a policy refusal. Every package failing is authentication, and the fix is the token, not the package.

Reading a policy refusal

A registry that enforces policy should say which rule refused the version. In Dependably Packages, every refusal is written to the audit log with the arm that refused it, so a 403 in CI has an explanation in the web UI: the licence did not match the allow list, the version carries an advisory above the severity ceiling, the package is on the CISA Known Exploited Vulnerabilities Catalog, the release is younger than the minimum age, or the package ships an install script your policy does not allow.

Each of those has a different right answer:

  • Licence. Either the licence is fine and the allow list needs it, or the package needs replacing. Do not widen the list to make a build pass.
  • Advisory severity or KEV listing. Move to a fixed version. If none exists yet, an exception with an expiry is the honest choice; a permanent exception is a hole.
  • Minimum release age. Wait. The rule exists because a compromised publish is usually noticed within days. Pinning the previous version is the normal workaround.
  • Install scripts. Check what the script does. Most packages that need one are build tools with native code; most that do not need one and have one are worth a second look.

Reserved names

The refusal nobody expects is on a package that was never meant to come from the public registry at all. If an internal package name is not reserved, a public package of the same name can win resolution and land in the build with no error anywhere. A registry that lets your organization reserve its namespaces turns that quiet substitution into a loud 403, which is the better outcome. If the refused name is one of yours, the fix is publishing the internal version, not allowing the public one.

What to change in CI

Two habits make the next refusal faster to handle:

  1. Print the registry's error body, not just the status. Package managers truncate it; the refusal reason is in there.
  2. Treat a policy 403 as a review item, not a retry. A rerun that succeeds because a colleague widened the rule is the outcome the rule was there to prevent.

The blocked packages page in the documentation walks through the same decision for each package manager, with the exact error text each one prints.