Troubleshooting restore failures: how NuGet finds sources and credentials through nuget.config, credential providers and CI service connections, package source mapping, lock files, caches, NuGet Audit, and protecting against dependency confusion.
First I read the exact error. NU1301 means a source couldn't be reached, 401 or 403 means authentication, and NU1101 means the package isn't on any configured source. It works on my machine because I have cached packages and stored credentials, while CI starts clean. Then I check which nuget.config CI actually uses, since config files are merged from the repo upwards. I make the repo's config explicit with <clear />, and I authenticate CI the supported way, for example the Azure Artifacts credential provider or a token from the pipeline's secret store, never a committed password. I also add package source mapping and lock files so restores are reproducible and protected against dependency confusion.
"I start by reading the exact code: NU1301 means a source couldn't be reached, 401 or 403 is authentication, and NU1101 means no configured source has the package. It works locally because my machine has cached packages, stored credentials and user-level config, while a clean CI agent has none of them. So I run
dotnet restore -v detailedanddotnet nuget list sourcein the pipeline to see which mergednuget.configit actually uses, because nothing builds until restore has writtenproject.assets.json.For example, after [we moved to hosted build agents], restore failed for our internal [Contoso.Abn] package even though it worked for every developer. I needed to get builds green and understand why. The detailed log showed CI only knew about nuget.org; the internal feed lived in personal and machine config. I committed a repo-level
nuget.configwith<clear />and package source mapping, added theNuGetAuthenticatetask with Reader permission on the [Azure Artifacts] feed, and turned on lock files with--locked-mode. Builds were green [the same day], restores became reproducible, and we closed a dependency confusion risk.I never commit a token in
nuget.config; credentials come from the credential provider or the pipeline's secret store. Clearing the cache is a diagnostic step, not a fix, and the mistake I see is retrying that without reading the code. I leave NuGet Audit on, since the .NET 10 SDK checks transitive packages by default for net10.0 projects, anddotnet tool restoreuses the same feeds, so the same fix applies there. The real fix is making the repo self-describing, not copying one developer's settings into the pipeline."
| Code / message | Usually means |
|---|---|
NU1301: Unable to load the service index |
Source URL wrong, network or proxy blocked, TLS issue, or an auth failure surfaced as a load failure |
401 Unauthorized / 403 Forbidden |
Missing, expired or insufficient credentials (for example the build identity lacks feed read permission) |
NU1101: Unable to find package |
No configured source has it, or source mapping excludes the source that does |
NU1102 / NU1103 |
The package exists but not the requested version (or only prerelease) |
NU1004 |
Lock file out of date in locked mode |
Your machine has packages in the global packages folder from earlier restores, credentials saved by Visual Studio or the credential provider, and possibly a user-level nuget.config with extra sources. A clean CI agent has none of these: user-level and machine-level settings from developer laptops simply do not exist there. CI is the honest test of what the repository itself declares.
NuGet merges nuget.config files from the solution folder up to the drive root, then the user-level and machine-level files. Run dotnet restore -v detailed and look for the list of config files and sources used, or dotnet nuget list source in the repo folder. Commit a repo-level nuget.config with <clear /> so only the listed sources apply everywhere.
NuGet authenticates through credential providers and packageSourceCredentials; you do not hand-craft HTTP headers.
NuGetAuthenticate@1 task before restore; it installs the Azure Artifacts credential provider and supplies a token for the pipeline identity. Grant that identity Reader on the feed (and check project-scoped feed permissions after moving projects).packageSourceCredentials (NuGet expands %VAR% there), or run dotnet nuget update source <name> --username ... --password "$TOKEN" --store-password-in-clear-text inside the job only. For Azure Artifacts, the credential provider can also read a token from VSS_NUGET_EXTERNAL_FEED_ENDPOINTS.nuget.config.<packageSourceMapping>) says Contoso.* comes only from the internal feed and everything else from nuget.org, blocking dependency confusion attacks where an attacker publishes a same-named package publicly.RestorePackagesWithLockFile) plus dotnet restore --locked-mode in CI fail the build if the graph changes unexpectedly.NuGetAuditMode defaults to all); older target frameworks still default to direct references only unless you set it.dotnet nuget locals all --clear) is a diagnostic step to rule out a corrupt cache, not a fix.A new pipeline failed with NU1301 on an internal Contoso.Abn package while every developer could restore. The repo had no nuget.config; developers had the internal feed in their user-level config, and CI only had nuget.org. Worse, a public package with a similar name existed. We committed a nuget.config with <clear />, both sources and source mapping, added NuGetAuthenticate, and gave the pipeline identity feed access. Restores became identical everywhere and the confusion risk was closed.
The interviewer wants a methodical troubleshooter with supply chain awareness.
Green flags
Red flags
nuget.config. repo/nuget.config \
repo/../nuget.config | merged, closest file first;
(each parent folder up | <clear /> discards every source
to the drive root) | defined in files further up
~/.nuget/NuGet/NuGet.Config | user level (developer laptops only)
machine-wide config / machine level (old self-hosted agents)
|
v
effective: packageSources + packageSourceMapping + packageSourceCredentials
|
v
for each package id --> only the source(s) its mapping allows
|
v
GET <source>/v3/index.json (service index)
|
+-- network, DNS, proxy or TLS failure --> NU1301
+-- 401 / 403 --> ask credential providers:
| 1. packageSourceCredentials in config (can use %ENV_VAR%)
| 2. Azure Artifacts credential provider
| (NuGetAuthenticate task, VSS_NUGET_EXTERNAL_FEED_ENDPOINTS)
| 3. stored credentials (Visual Studio, interactive sign-in)
| none work --> 401 / NU1301
v
search for the id and version --> not found --> NU1101 / NU1102 / NU1103
|
v
download .nupkg --> verify hash and signature --> ~/.nuget/packages
Every box on that path behaves differently on a developer laptop and a clean CI agent. That is the whole story behind "works on my machine".
| Code | Meaning | Check first |
|---|---|---|
| NU1301 | Couldn't load the service index | Source URL, DNS, proxy, firewall, TLS inspection, or an auth failure reported as a load failure |
| 401 Unauthorized | No valid credentials | Did NuGetAuthenticate run before restore? Is the token set and not expired? |
| 403 Forbidden | Credentials valid but not allowed | Does the pipeline identity have Reader on the feed (and on upstream views)? |
| NU1101 | Package not found on any allowed source | Package name, source mapping patterns, upstream sources |
| NU1102 | Package found, version not found | Version range, whether the version was published or unlisted |
| NU1103 | Only prerelease versions exist | Version range, or publish a stable version |
| NU1004 | Lock file out of date in locked mode | Someone changed a reference without updating packages.lock.json |
| NU1100 | Couldn't resolve a dependency for a framework | Target framework compatibility |
| NU1901 to NU1904 | Known vulnerability (low to critical) | NuGet Audit; upgrade the package |
| Developer laptop | Clean CI agent | |
|---|---|---|
~/.nuget/packages |
Full of packages from earlier restores | Empty (unless cached) |
| HTTP cache | Recent feed responses | Empty |
User-level NuGet.Config |
Often has extra sources added years ago | Doesn't exist |
| Credentials | Stored by Visual Studio or the credential provider | Only what the pipeline provides |
| Network | Corporate VPN, proxy settings | Different egress, maybe no access to internal networks |
| Identity | Your user, with feed access | A pipeline identity that may lack permissions |
A restore can succeed locally without contacting the feed at all, because every package is already in the global packages folder. CI is the honest test of what the repository declares.
dotnet restore -v detailed (or -v diag) in the pipeline. It lists the config files it read, the sources it used and each HTTP call.dotnet nuget list source in the repo folder, locally and in CI, and compare.dotnet nuget locals all --clear (or a fresh container) and no user-level config, for example docker run --rm -v "$PWD:/src" -w /src mcr.microsoft.com/dotnet/sdk:10.0 dotnet restore.| Pipeline and feed | Approach |
|---|---|
| Azure Pipelines + Azure Artifacts (same organisation) | NuGetAuthenticate@1 before restore; grant the build identity Reader on the feed |
| Azure Pipelines + feed in another organisation | NuGetAuthenticate@1 with a service connection |
| GitHub Actions + GitHub Packages | GITHUB_TOKEN (or a token with read:packages) referenced from packageSourceCredentials through an environment variable |
| GitHub Actions + Azure Artifacts | Azure Artifacts credential provider with VSS_NUGET_EXTERNAL_FEED_ENDPOINTS from a secret, or a read-scoped token referenced from config |
| Any CI + a token-based feed | dotnet nuget update source <name> --username x --password "$TOKEN" --store-password-in-clear-text, run inside the job only, never committed |
Tokens should be short-lived and read-only. Workload identity federation or the platform's built-in identity is better than a long-lived personal access token.
Without source mapping: With source mapping:
Contoso.Abn requested Contoso.Abn requested
-> NuGet asks EVERY source -> pattern Contoso.* -> contoso feed ONLY
-> an attacker has published a -> nuget.org is never asked for it
package called Contoso.Abn on
nuget.org the internal package is always used
-> if the range floats (1.*), the
attacker publishes a higher
version, or publishes the exact
version you use, restore can take
the public one
-> the attacker's build logic runs on
your CI agent
Without mapping, NuGet doesn't treat any source as authoritative for a package id, so the outcome depends on versions and which source answers. Source mapping makes the source for each package explicit. With mapping in place, NU1101 for an internal package often means the pattern doesn't cover it, so check patterns before anything else. An internal Azure Artifacts feed with upstream sources for nuget.org gives a single controlled entry point and keeps a copy of every public package you use.
| Setting | Effect |
|---|---|
RestorePackagesWithLockFile=true |
Restore writes packages.lock.json with exact versions and hashes |
dotnet restore --locked-mode (or RestoreLockedMode=true in CI) |
Fails with NU1004 if the graph would change |
Floating versions such as 4.* |
Resolved once and recorded, so later restores are reproducible |
Commit the lock files and use locked mode in CI, so a new package version or a compromised feed can't change what you build without a reviewed commit.
| Cache | Location | Notes |
|---|---|---|
| Global packages folder | ~/.nuget/packages |
Extracted packages shared by all projects; safe to cache in CI |
| HTTP cache | Shown by dotnet nuget locals http-cache --list |
Can hide a newly published version for up to about 30 minutes; --no-http-cache bypasses it |
| Temp | Shown by dotnet nuget locals temp --list |
Scratch space |
dotnet nuget locals all --clear is a diagnostic step to rule out a corrupt cache, not a fix for configuration or authentication problems.
Restore checks packages against known vulnerability data and reports NU1901 to NU1904. With the .NET 10 SDK, projects targeting .NET 10 or later audit transitive packages by default (NuGetAuditMode is all); older target frameworks audit direct references only unless you set it. Run dotnet list package --vulnerable --include-transitive for a full report, and treat high and critical findings as errors on main branches.
nuget.config.trustedSigners in nuget.config can require signatures from specific repositories or authors.NuGet ships with the SDK, so audit defaults and source mapping features can change between SDK versions. Read the NuGet release notes when you move to the .NET 11 SDK.
nuget.config from the code example to a solution and run dotnet restore -v detailed; find the list of config files and sources in the output.dotnet restore --locked-mode to see NU1004.dotnet list package --vulnerable --include-transitive.Repo-level nuget.config with explicit sources and source mapping (no credentials committed):
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="contoso" value="https://pkgs.dev.azure.com/contoso/_packaging/internal/nuget/v3/index.json" />
</packageSources>
<packageSourceMapping>
<packageSource key="contoso">
<package pattern="Contoso.*" />
</packageSource>
<packageSource key="nuget.org">
<package pattern="*" />
</packageSource>
</packageSourceMapping>
</configuration>
Directory.Build.props enabling lock files:
<Project>
<PropertyGroup>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
</Project>
Azure DevOps pipeline steps:
steps:
- task: UseDotNet@2
inputs:
useGlobalJson: true
- task: NuGetAuthenticate@1
- script: dotnet restore Orders.slnx --locked-mode
displayName: Restore
Diagnostics:
dotnet restore -v detailed # shows config files, sources and why a package failed
dotnet nuget list source # effective sources for this folder
dotnet nuget locals all --list # cache locations
dotnet list package --vulnerable --include-transitive
Mistake: clearing the NuGet cache and retrying without reading the error code.
NU1301, 401, 403 and NU1101 have different causes, so retrying wastes time and the cache was rarely the problem.
Instead: read the code and source name, then run dotnet restore -v detailed to see the config files, sources and HTTP calls.
Mistake: relying on a user-level or machine-level nuget.config.
Sources and credentials that exist only on developer laptops or an old self-hosted agent are invisible to a clean CI agent.
Instead: commit a repo-level nuget.config with <clear /> and every source the build needs.
Mistake: assuming a local restore proves the feed works.
With packages already in ~/.nuget/packages, a local restore may never contact the feed.
Instead: reproduce with a clean cache or a fresh SDK container before concluding the feed is fine.
Mistake: committing feed passwords or personal access tokens in nuget.config.
Everyone with repository access, and every fork, now has the credential.
Instead: use the platform's authentication (NuGetAuthenticate@1, credential provider, GITHUB_TOKEN) or reference a secret through an environment variable.
Mistake: forgetting that the pipeline identity needs feed permissions. Moving a project or feed changes the identity or scope, and restore fails with 401 or 403. Instead: grant the build identity Reader on the feed and its upstream views, and check after every move.
Mistake: mixing public and private sources without source mapping.
An attacker can publish a same-named package with a higher version publicly, and restore picks it: dependency confusion.
Instead: map internal prefixes such as Contoso.* to the internal feed only, or route everything through one internal feed with upstream sources.
Mistake: floating versions without a lock file.
* versions resolve to whatever is newest, so CI restores something different from yesterday.
Instead: pin versions and enable lock files with --locked-mode in CI.
Mistake: suppressing NuGet Audit warnings.
Known vulnerabilities stay in production, and new transitive warnings after retargeting to net10.0 get ignored.
Instead: upgrade the vulnerable package (or the parent that brings it in), and treat high and critical findings as errors on main.
Mistake: ignoring the network path. Hosted agents can't reach internal feeds behind a VPN, and TLS inspection proxies break certificate validation. Instead: check DNS and connectivity from the agent, use a feed reachable from CI, or a self-hosted agent inside the network.
Restore is the front door of your software supply chain. A senior engineer makes it explicit and boring: one committed nuget.config, an internal feed with upstream sources so all packages flow through one controlled place, source mapping, lock files in CI, read-only short-lived credentials from the platform, and NuGet Audit warnings treated as work items. The "works on my machine" symptom is usually a hidden dependency on personal configuration, so the fix is to make the repo self-describing rather than to copy one developer's settings into the pipeline.
Restore works out the full NuGet package graph, downloads packages to the global packages folder and writes obj/project.assets.json. Build compiles against those assets into bin/<Configuration>/<TFM>. Test runs the test projects against the built assemblies. Publish produces a deployable folder with the app, its dependencies and runtime config, in Release by default since .NET 8. Each later command restores and builds implicitly, so in CI I restore once and pass --no-restore and --no-build. That way every step uses exactly the same outputs and nothing is repeated. The configuration has to match across steps.
dotnet pack builds the project and produces a .nupkg whose metadata comes from MSBuild properties in the project: PackageId, Version, Authors, Description, a licence expression, a repository URL and a README. I set the version from CI using Semantic Versioning, so a breaking change means a new major version. I publish symbols as a .snupkg with Source Link, so consumers can step into the exact source. I turn on package validation to catch accidental breaking changes. Then dotnet nuget push uploads to nuget.org or an internal feed, and organisations that need it can sign packages with dotnet nuget sign.
A .NET tool is a console app shipped as a special NuGet package. Global tools (dotnet tool install -g) are installed per user and land on your PATH. Local tools are pinned per repository in a manifest, .config/dotnet-tools.json, created with dotnet new tool-manifest. Anyone who clones the repo runs dotnet tool restore and gets exactly the same versions. For team and CI work I prefer local tools, because the version lives in source control next to the code that depends on it.
How to tell it (STAR)
**Situation:** After we moved to hosted build agents, restore failed for our internal packages even though it worked for every developer. **Task:** I needed to get builds green and understand why. **Action:** A detailed restore log showed CI only knew about nuget.org; the internal feed lived in personal and machine config. I committed an explicit `nuget.config` with source mapping, set up the supported authentication task and feed permissions, and turned on lock files. **Result:** Builds were green the same day, restores became reproducible, and we closed a dependency confusion risk [mention any security review outcome].
Last updated 1 Nov 2026.