Overview
NuGet.Services.KeyVault is a shared library that wraps the Azure Key Vault SDK behind a clean abstraction layer, enabling NuGet services to retrieve and manage secrets without being coupled to the underlying Azure SDK types. It defines a set of interfaces (ISecretReader, ISecretWriter, ISecretInjector, and their caching variants) that allow consuming projects to work with secrets uniformly, whether the backing store is a real Key Vault vault, an in-memory cache, or a no-op stub used in local development and testing.
The library ships two distinct caching strategies built on top of ISecretReader. The first, CachingSecretReader, uses time-based expiry: secrets are held in a ConcurrentDictionary and re-fetched from Key Vault after a configurable interval (default 24 hours) or when they are within a configurable window of their Key Vault expiration date (default 30 minutes before expiry). The second, RefreshableSecretReader, separates the concern of when to refresh from the concern of reading: the cache is populated on first access and is only updated when Refresh() or RefreshAsync() is explicitly called, which suits long-running services that want to pre-warm secrets during startup and refresh them on a background timer.
Authentication with Key Vault is handled inside KeyVaultReader and is determined entirely by the KeyVaultConfiguration provided at construction time. Three modes are supported: managed identity (using ManagedIdentityCredential in production or DefaultAzureCredential in DEBUG builds), client certificate with standard authentication (ClientCertificateCredential), and client certificate with the SendX5c flag set, which instructs Azure Active Directory to include the full certificate chain in the authentication request. The SecretClient is created lazily so that configuration errors surface at first use rather than at startup.
Role in System
ISecretReader, ISecretInjector) rather than on concrete types, which allows the caching and refresh strategy to be swapped or composed at the DI registration site without changing call sites.
Two Caching Strategies
CachingSecretReader refreshes automatically on a time-based schedule. RefreshableSecretReader only refreshes when explicitly told to, making it safe for ASP.NET startup scenarios where async vault calls can cause deadlocks.Secret Injection
SecretInjector scans arbitrary strings for tokens framed with $$ (e.g., $$MySecretName$$) and replaces them with the resolved secret value, enabling secrets to be embedded in configuration strings.Three Auth Modes
Supports managed identity, client certificate, and client certificate with full chain (
SendX5c). In DEBUG builds, managed identity falls back to DefaultAzureCredential for developer workstation convenience.Null-Object Pattern
EmptySecretReader returns the secret name itself as its value, providing a safe no-op implementation for local development and unit tests that do not need real vault connectivity.Key Files and Classes
Dependencies
NuGet Package References
Internal Project References
This project has no internal project references. It is a leaf library.Notable Patterns and Implementation Details
The
SecretClient inside KeyVaultReader is wrapped in a Lazy<T>. The client is not created until the first secret read call, which means authentication failures (bad certificate, wrong tenant ID) appear at first use, not at application startup.CachingSecretReader uses two independent freshness criteria. A cached secret is considered outdated if either (a) more than refreshIntervalSec seconds have passed since it was cached, or (b) the secret’s Key Vault expiration timestamp is within refreshIntervalBeforeExpirySec seconds of the current UTC time. This prevents serving an expired secret value even if the time-based TTL has not yet elapsed.RefreshableSecretReaderFactory shares a single ConcurrentDictionary<string, ISecret> instance across all ISecretReader objects it creates. This means calling RefreshAsync on the factory refreshes secrets for every consumer that was given a reader from that factory instance.CertificateUtility.FindLatestActiveCertificateBySubject selects the certificate with the latest NotAfter date when multiple certificates share the same subject distinguished name, with NotBefore as a tiebreaker. This supports rolling certificate deployments where both the old and new certificates exist in the store simultaneously.