Rolling NuGet package signing enforcement across a large .NET estate sounds like a six-week project when you plan it on a whiteboard. When I actually did it, spread across about 420 repositories and two build systems, the calendar version was closer to nine months. The cryptography is not the hard part. The hard part is every legacy project file that predates PackageReference, every internal feed that never bothered with signatures, and every build agent whose trust store was last touched in 2019. I want to walk through how one of those rollouts actually goes, because the Microsoft documentation covers the happy path in loving detail and stops right where the real work begins.
Why we started
The trigger, honestly, was an audit finding. A pen-test vendor noted that our build agents pulled packages from both nuget.org and two internal feeds without verifying any signatures, and that a compromise of either of our Azure DevOps artifact feeds would let an attacker inject unsigned packages that our builds would consume without complaint. The finding cited the dependency confusion research from 2021 and the specific pattern of internal feeds without upstream source filtering. The remediation ask was simple on paper: enforce signature validation on all NuGet restore operations, across all build agents, for all projects.
The project charter I wrote in January 2024 had a target completion of end of Q2. We shipped the first fully enforced team in March, the last in November, and the cleanup of exceptions trailed into the following year.
The four populations you actually have
Once I started mapping the estate I realised that the 420 repositories split into four very different populations. The first was modern .NET 6 and .NET 8 projects using SDK-style project files and PackageReference. These are the easy ones. Signature validation is a flag in nuget.config or a property in the central config, and the tooling works out of the box with reasonably recent NuGet client versions. NuGet 6.7, shipped alongside .NET 8 in November 2023, had the signature validation story essentially in its final form.
The second was older .NET Framework 4.6 through 4.8 projects still using packages.config. These projects can be signed-aware, but the tooling path is different. The nuget.exe command line needs to be recent enough, the MSBuild targets need to be aware of signature verification, and the packages.config format has no native way to express a trusted signer list at project scope. You end up configuring signing at the machine level via nuget.config or environment variables, and you pray that developers have not overridden it locally.
The third was projects pinned to very old dependencies that were unsigned when originally published and never received signed republications. A lot of these live in the internal-tooling and utility-library space, where the maintainer may have left the company. Every one of these needs an exemption or a migration path.
The fourth was mixed-language repositories where .NET is only part of the build, and the CI configuration was written by the non-.NET team. These repositories often had custom nuget.exe invocations with flags that predated the signature verification era. Auditing them required reading each CI file by hand.
What we actually configured
The canonical mechanism is <trustedSigners> in nuget.config, which defines which author and repository signatures are acceptable. The minimum viable policy for us required a repository signature from nuget.org (certificate thumbprint 3F9001EA83C560D712C24CF213C3D312CB3BFF51EEE), a repository signature from our Azure DevOps artifact feed, and an author signature from any of about sixty known-good author certificates we whitelisted based on the top 200 packages by install count in our estate.
The enforcement flag is signatureValidationMode=require, which causes NuGet restore to fail if any package lacks an acceptable signature. The default is accept, which verifies signatures when present but tolerates their absence. The jump from accept to require is where a project either works or stops working, and the population that stops working is precisely the old-dependencies group I mentioned above.
For the central rollout we put the nuget.config on the build agents, not in the repositories, and we used a clientCertificates-plus-trustedSigners configuration that was identical across all agents. We considered repository-local nuget.config but decided that centralising the trust policy made auditing easier, and in retrospect that was correct.
The certificate drama
About six weeks in we hit the certificate expiration problem. NuGet signatures carry a certificate chain, and by default NuGet requires the signing certificate to be valid at verification time. When an old package was signed in 2018 with a certificate that expired in 2021, signature verification against a 2024 clock fails unless the package was also timestamped at signing time by a trusted timestamp authority.
The good news is that almost all nuget.org packages published after 2018 carry RFC 3161 timestamps from DigiCert or a similar TSA, and NuGet verification accepts an expired certificate as long as the timestamp proves the signature was made while the certificate was valid. The bad news is that some old internal packages were signed without timestamps and simply cannot be verified today without allowing expired certificates, which weakens the policy.
We ended up with a two-tier policy. The strict tier, applied to production builds, requires valid-at-verification-time certificates or valid timestamps. The permissive tier, applied only to builds marked explicitly as legacy, allows expired certificates as long as the signature itself is valid. About forty repositories lived in the permissive tier for most of 2024 before being migrated or retired.
The failure modes nobody documents
The first surprise was that some packages on our internal feed had been mirrored from nuget.org without preserving the repository signature, which meant they had only an author signature when consumed via the internal feed. A strict require policy that expected both rejected them. Fixing the mirroring tool to preserve signatures was a ticket that took three sprints because the feed was owned by a different team.
The second surprise was that CVE-2024-0056, the .NET SqlClient vulnerability disclosed in January 2024, pushed a lot of teams to update their Microsoft.Data.SqlClient package right in the middle of our rollout. The updates were fine, but the parallel changes created merge conflicts and hid signature-related failures in noise.
The third surprise was that developer workstations running dotnet restore locally sometimes used a different nuget.config than the build agents, because the user-level config overrode the machine-level config. We shipped a PowerShell script that validated the effective nuget.config on developer machines and reported drift, and we added a pre-commit hook in the highest-value repositories.
How Safeguard Helps
Safeguard inventories the NuGet packages in every build in our estate and flags packages that fail signature verification against the tenant policy, which lets us detect drift from the nuget.config baseline without reading build logs. It tracks the certificate and timestamp metadata per package version, so a signature that was valid at sign time but has no timestamp surfaces as a finding with clear context. During our enterprise rollout the exception list lived in Safeguard with owner and expiry fields, turning what would have been a spreadsheet into a structured control. For ongoing enforcement the platform alerts when a project's effective trusted-signer policy diverges from the central standard.