Architecture Note: NuGet Package Restore in Automated Builds
Design a secure, reproducible NuGet restore process using mirrors, trust boundaries, and locked-mode verification to prevent supply chain risks in CI pipelines.
19 Aug 2025, 22:27 UTC

The Problem: Non-Deterministic Restores
In automated build pipelines, relying on direct connections to public galleries like nuget.org introduces three primary risks: non-deterministic builds due to floating versions, pipeline failure during public gallery outages, and security vulnerabilities from dependency confusion or tampered packages. The goal is a restore process that is immutable, verified, and isolated.
Requirements
To ensure build stability and security, the restore architecture must satisfy these requirements:
- Reproducibility: The exact same package versions must be resolved across every build of a specific commit.
- Integrity: Packages must be verified via SHA512 hashes to ensure the binary has not been altered.
- Isolation: The build agent should not have unrestricted outbound access to the public internet for dependency resolution.
- Fail-Fast Behavior: Any mismatch in version, hash, or authentication must halt the pipeline immediately.
Smallest Suitable Design
The minimal design that meets these requirements is a Read-Only Mirror Architecture. Instead of the agent querying the public gallery, it queries an internal proxy or hosted repository (such as Azure Artifacts, Nexus, or Artifactory) that contains only vetted, approved packages.
Core Components
- Build Agent: Executes the
dotnet restoreor MSBuild-t:Restorecommand. - Trusted Mirror: An internal feed acting as the single source of truth.
- Global-Packages Folder: The local cache (
%USERPROFILE%\.nuget\packageson Windows) where NuGet stores packages and their corresponding hash files. - Lock File (project.assets.json): A generated file that records the exact resolution graph and hashes for the project.
Configuration Example
The following NuGet.Config should be placed at the repository root. Note that the public gallery is explicitly disabled to prevent the agent from bypassing the mirror.
<configuration>
<packageSources>
<add key="InternalMirror" value="https://company.pkgs.visualstudio.com/_packaging/MainFeed/nuget/v3/index.json" />
</packageSources>
<disabledPackageSources>
<add key="nuget.org" value="true" />
</disabledPackageSources>
<packageSourceCredentials>
<InternalMirror>
<username>build_service_account</username>
<password>%NUGET_FEED_TOKEN%</password>
</InternalMirror>
</packageSourceCredentials>
</configuration>
Risk Note: Never commit plain-text tokens to source control. Use environment variable substitution (e.g., %NUGET_FEED_TOKEN%) injected by your CI provider's secret manager.
Trust and Data Boundaries
The trust boundary is strictly defined by the <packageSources> section. Any source not explicitly listed or specifically enabled is treated as untrusted. Data flows as follows:
- The agent reads
NuGet.Configto identify the trusted endpoint. - NuGet requests metadata and the
.nupkgfile from the mirror. - The downloaded package is verified against the SHA512 hash. If the hash does not match the one recorded in the lock file or the global-packages folder, the process terminates.
- The
project.assets.jsonfile is updated only after successful verification.
Operational Checks
To verify the implementation on a clean build agent, perform these checks:
- Network Isolation: Run
dotnet restoreand monitor network logs to ensure no traffic is sent tonuget.org. - Cache Audit: Inspect the global-packages folder and verify that the
.nupkg.sha512files exist for all restored dependencies. - Exit Code: Ensure the restore command returns exit code 0. Any non-zero exit must trigger a pipeline failure.
Failure Modes
NuGet provides specific error codes that allow the pipeline to diagnose the failure point quickly:
| Error Code | Condition | Root Cause |
|---|---|---|
NU1107 |
Version Conflict | Incompatible version ranges requested by different projects. |
NU1605 |
Hash Mismatch | The package binary was altered or corrupted on the mirror. |
NU1301 |
Auth Failure | The agent's token is expired or lacks permissions for the mirror. |
Conditions for Redesign
This architecture should be re-evaluated if:
- Poly-repo Scaling: The organization moves to a structure where teams require autonomous, external pre-release packages that cannot be mirrored centrally.
- Toolchain Migration: The project migrates to a build system (e.g., Bazel) that does not use the MSBuild restore target.
- Latency Constraints: Mirror latency becomes a critical bottleneck, necessitating a hybrid approach with strict hash-locking for external sources.
Practical Verification: Testing Integrity
To confirm that the system prevents tampered packages from entering the build, simulate a corruption event:
- Download a vetted package from the mirror.
- Modify a single byte within the package's DLL.
- Upload the modified package back to the mirror (or a test feed) while keeping the original hash in the
project.assets.jsonlock file. - Run
dotnet restore. The build must fail with NU1605.
Limitations
A trusted mirror prevents external tampering but does not protect against a compromised package that was vetted and uploaded to the mirror originally. This requires complementary vulnerability scanning (SCA). Additionally, to prevent silent updates from floating versions (e.g., 1.0.*), use the --locked-mode flag (available in .NET 6+ SDK):
dotnet restore --locked-mode
This ensures that if the lock file needs to be updated to resolve a dependency, the restore fails rather than silently updating the version.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.