Skip to main content

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

The library sits between NuGet application code and the Azure Key Vault service. Consuming projects depend on the interfaces (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.
RefreshableSecretReaderSettings.BlockUncachedReads exists specifically to prevent deadlocks in ASP.NET applications. When set to true, any attempt to read a secret that is not already in the cache throws InvalidOperationException instead of making a live vault call. The intended pattern is: populate the cache during startup with BlockUncachedReads = false, then flip the flag to true before the application begins handling requests.
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.
SecretInjector uses $$ as its default frame delimiter. A custom delimiter can be supplied via the two-argument constructor. The parser handles duplicate secret references in a single string efficiently by collecting unique names into a HashSet<string> before making any vault calls.
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.