NuGet restore 403 (Forbidden) - Under-Scoped Feed Token in CI
Unlike a 401 (no/invalid credentials), a 403 means NuGet authenticated successfully but the identity is not allowed to read the feed. The token is valid but under-scoped, or the account lacks feed access.
What this error means
Restore fails on a private source with 403 (Forbidden) rather than 401. It is deterministic - the same job fails identically every run - and persists until the token gains read permission, distinguishing it from a transient 5xx.
error : Response status code does not indicate success: 403 (Forbidden).
error : Unable to load the service index for source
https://nuget.pkg.github.com/contoso/index.json.Common causes
Token authenticated but lacks read scope
A PAT or app token valid for the org may not have the Packaging/read:packages scope the feed requires, so the feed accepts the identity but forbids the read.
Identity has no permission on the specific feed
The user or service account behind the token is not a member/reader of the feed (a private Azure Artifacts feed or a GitHub Packages repo it cannot see), producing 403 rather than 401.
How to fix it
Grant the token the read scope
Re-issue the token with the feed-read permission, then re-register the source.
# GitHub Packages: token needs read:packages
dotnet nuget add source https://nuget.pkg.github.com/contoso/index.json \
--name github --username USER \
--password "$GH_PACKAGES_TOKEN" --store-password-in-clear-textConfirm the identity can read the feed
- Verify the service account/user is a reader on the specific feed, not just the org.
- Check the token has not been scoped down to a different feed or resource.
- Re-run restore and confirm the 403 is gone before treating it as fixed.
How to prevent it
- Scope feed tokens to least privilege but include the read permission the feed needs.
- Grant the CI identity explicit reader access on each private feed.
- Track token expiry/rotation so access does not silently lapse to a 401/403.