Using NuGet Locked Mode for Deterministic .NET Builds
Configure NuGet restore with `-LockedMode` and a lock file to guarantee that CI pipelines restore exactly the versions declared in your project, reducing surprise version drift and supply‑chain risk.
03 Aug 2026, 17:18 UTC

Problem: Non‑deterministic restores break reproducible builds
When a .NET project relies on NuGet’s default restore behavior, transitive dependencies can resolve to newer patch or minor versions that were not explicitly declared. Over time this leads to builds that pass on one agent and fail on another, or worse, introduces unverified code from a compromised package published to a public feed.
Takeaway
Enable NuGet’s locked‑mode restore (or use a packages.lock.json) so the restore step fails if any package version deviates from the lock file. Combine this with feed validation and exit‑code checks to create a small, trust‑boundary‑aware design that yields deterministic, reproducible builds.
Requirements
- Every build must restore the exact versions declared in the project’s direct dependencies and their transitive closure.
- The restore step must be auditable: logs should show which packages were resolved and the pipeline must abort on any warning or error.
- External feeds must be treated as untrusted until package signatures or checksums are verified.
Smallest Suitable Design
The minimal change to achieve the requirements is to invoke NuGet restore with the -LockedMode flag (or its dotnet alias --locked-mode) and ensure a lock file exists.
- If a
packages.lock.jsonis present,dotnet restore --locked-moderestores exactly the versions recorded in that file. - If no lock file exists, the command fails, forcing the team to generate one first (e.g., via
dotnet restore --locked-modeon a trusted machine).
This design adds no extra tools beyond the existing .NET SDK and works with any NuGet feed (internal, upstream, or nuget.org).
Trust/Data Boundaries
Treat the NuGet feed as an external dependency:
- For internal feeds, require package signing and verify the signature during restore (
dotnet restore --locked-mode --interactivewill prompt for untrusted signatures; in CI you can enforce--no-interactionand let the step fail if a signature is missing). - For public feeds, consider mirroring through an internal proxy that validates signatures before allowing packages to be served.
The lock file itself is considered trusted data because it is version‑controlled alongside the source code.
Operational Checks
- Check the exit code of the restore step; any non‑zero code aborts the pipeline.
- Scan restore logs for warnings such as "Package X was not found in the lock file" – treat these as failures.
- After restore, compute a hash of the restored package folder (e.g.,
sha256sum obj/project.assets.json) and compare it to a known good hash stored in the repository or generated during a trusted baseline build. - Optionally, fail the pipeline if the restore time deviates significantly from baseline, which can indicate a network fallback or feed switch.
Failure Modes
- Network interruption – restore cannot reach the feed; step exits with non‑zero, pipeline aborts.
- Feed authentication failure – missing or invalid credentials cause a 401/403; same abort behavior.
- Package corruption – checksum mismatch triggers restore failure.
- Version conflict – if a transitive dependency is not pinned in the lock file, locked‑mode restore fails, alerting the team to add an explicit version.
Each of these results in a clear, actionable error rather than a silent drift.
Conditions That Would Change the Design
- Introducing floating version ranges (e.g.,
1.2.*) would make the lock file insufficient; you would need to accept a less strict restore or generate a new lock file on every build. - Using upstream sources without validation would move the trust boundary outward, requiring additional signature verification or a stricter internal proxy.
- Disabling signature verification to speed up restores would increase supply‑chain risk; you would then need to rely on other controls such as vulnerability scanning of the lock file.
Practical Example: GitHub Actions Workflow
name: Build
on:
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Restore packages (locked mode)
run: dotnet restore --locked-mode --no-interaction
- name: Verify lock‑file hash
run: |
HASH=$(sha256sum obj/project.assets.json | cut -d' ' -f1)
echo "Lock file hash: $HASH"
# Compare to a hash stored in the repository (e.g., in a file .lockhash)
if [ "$HASH" != "$(cat .lockhash)" ]; then
echo "Lock file hash mismatch!"
exit 1
fi
- name: Build
run: dotnet build --configuration Release --no-restore
Explanation:
- The
dotnet restore --locked-mode --no-interactionstep fails if any package version differs frompackages.lock.jsonor if the lock file is missing. - The subsequent hash check ensures that the restored assets file matches the committed version, detecting any silent corruption or feed switch.
- If the network is unavailable or a signature is invalid, the restore step returns a non‑zero exit code, causing the job to stop.
Limitations and How to Check the Result
Locked‑mode restore requires that every transitive dependency be explicitly present in the lock file. If you add a new direct dependency that pulls in a new transitive package, the restore will fail until you regenerate the lock file (e.g., by running dotnet restore --locked-mode on a trusted machine and committing the updated packages.lock.json).
To verify that the design is working:
- Run the restore step on a clean agent and confirm the exit code is 0.
- Introduce a deliberate version mismatch in the lock file (e.g., change a version number) and observe that the restore step fails with a message like "Package X version Y is not allowed in locked mode."
- Disconnect the network or provide bogus credentials and ensure the step fails, proving the trust boundary is enforced.
These checks give confidence that the pipeline will abort on any deviation from the exact, vetted set of packages.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.