Technology Apr 29, 2026 · 6 min read

SecretVault: One Interface to Rule All Your Cloud Secrets in .NET 9

Every .NET project that touches the cloud eventually runs into the same problem: secret sprawl. You start with AWS Secrets Manager. Then a new service needs Azure Key Vault. Someone spins up a HashiCorp Vault. Before long, your codebase is littered with provider-specific SDKs, inconsistent error ha...

DE
DEV Community
by Naimul Karim
SecretVault: One Interface to Rule All Your Cloud Secrets in .NET 9

Every .NET project that touches the cloud eventually runs into the same problem: secret sprawl.

You start with AWS Secrets Manager. Then a new service needs Azure Key Vault. Someone spins up a HashiCorp Vault. Before long, your codebase is littered with provider-specific SDKs, inconsistent error handling, and zero ability to swap providers without rewriting half your infrastructure code.

I built SecretVault to fix this. One interface. Four providers. Zero lock-in.

The Problem in Plain Code

Here's what most codebases look like today:

// Somewhere in your AWS service
var awsClient = new AmazonSecretsManagerClient();
var response = await awsClient.GetSecretValueAsync(new GetSecretValueRequest
{
    SecretId = "prod/db-password"
});
var secret = response.SecretString;

// Somewhere else, for Azure
var kvClient = new SecretClient(vaultUri, new DefaultAzureCredential());
var azureSecret = await kvClient.GetSecretAsync("db-password");
var value = azureSecret.Value.Value;

Different SDKs. Different error types. Different mental models. And when you want to add caching or rotate secrets? You wire it up manually — for each provider — every time.

The SecretVault Way

// That's it. Same line. Any provider.
var password = await secrets.GetSecretAsync("db-password");

ISecretManager is the only interface your application code ever sees. Swap AWS for Azure in one line of DI configuration — your business logic never changes.

What's in the Box

SecretVault ships as 6 focused NuGet packages — install only what you need:

Package What it does
SecretVault.Core The ISecretManager interface + caching + chaining
SecretVault.Aws AWS Secrets Manager provider
SecretVault.Azure Azure Key Vault provider
SecretVault.HashiCorp HashiCorp Vault KV v2 provider
SecretVault.Gcp Google Cloud Secret Manager provider
SecretVault.Extensions.Hosting ASP.NET Core DI + IConfiguration + health checks
dotnet add package SecretVault.Core
dotnet add package SecretVault.Aws
dotnet add package SecretVault.Extensions.Hosting

Quick Start: AWS

using Amazon.SecretsManager;
using SecretVault.Aws;

var client = new AmazonSecretsManagerClient();
ISecretManager secrets = new AwsSecretManager(client, logger);

// Fetch a raw string
var apiKey = await secrets.GetSecretAsync("prod/api-key");

// Fetch and deserialize JSON directly into a typed object
var dbConfig = await secrets.GetSecretAsync<DatabaseConfig>("prod/db-config");

Switching to Azure? One line changes:

// Before
ISecretManager secrets = new AwsSecretManager(client, logger);

// After
ISecretManager secrets = new AzureKeyVaultSecretManager(kvClient, logger);

Your application code above stays identical.

Built-in Caching

Cloud secret APIs have rate limits and latency. Fetching the same secret on every request is wasteful. Wrap any provider with CachedSecretManager:

var cached = new CachedSecretManager(
    inner: secrets,
    cache: memoryCache,
    logger: logger,
    options: new CachedSecretManagerOptions
    {
        AbsoluteExpiration = TimeSpan.FromMinutes(10)
    });

The cache is automatically invalidated on writes and deletes — so you never serve stale secrets after a rotation.

Fallback Chaining

Running a hybrid cloud? Or want a local fallback for development? ChainedSecretManager tries providers in order and returns the first successful result:

var chained = new ChainedSecretManager(
    [awsSecrets, localSecrets],
    logger);

// Tries AWS first, falls back to local dev secrets if not found
var value = await chained.GetSecretAsync("api-key");

This is great for local development too — point your fallback at environment variables or a local JSON file without touching your production code path.

Secret Rotation

All provider implementations also implement ISecretRotationManager, giving you first-class rotation support:

ISecretRotationManager rotatable = new AwsSecretManager(client, logger);

var result = await rotatable.RotateSecretAsync(
    secretName: "prod/db-password",
    newValue: GenerateSecurePassword(),
    gracePeriod: TimeSpan.FromHours(1)  // old version stays accessible during rollover
);

Console.WriteLine($"Rotated to version {result.NewVersion} at {result.RotatedAt}");
// Optionally: notify dependent services, invalidate connection pools, etc.

You can also list all versions and retrieve a specific one:

var versions = await rotatable.ListVersionsAsync("prod/db-password");
var oldValue = await rotatable.GetSecretVersionAsync("prod/db-password", versions[1].Version);

ASP.NET Core Integration

Load secrets into IConfiguration at startup

Pull secrets directly into the config pipeline so they're accessible via IConfiguration just like appsettings.json:

// Program.cs
builder.Configuration.AddSecretVault(secretManager, [
    "db-connection-string",
    "stripe-api-key",
    "sendgrid-api-key"
]);

// Then use them as normal config
var connStr = builder.Configuration["db-connection-string"];

Register via Dependency Injection

builder.Services.AddSecretVault(secretManager, options =>
{
    options.AbsoluteExpiration = TimeSpan.FromMinutes(5);
});

Inject at runtime

public class PaymentService(ISecretManager secrets)
{
    public async Task ProcessPayment(decimal amount)
    {
        var stripeKey = await secrets.GetSecretAsync("stripe-api-key");
        // use it
    }
}

Health checks

builder.Services.AddHealthChecks()
    .AddSecretVaultHealthCheck("__probe__", name: "secrets");

This adds a /health endpoint entry that pings your secret provider on every health check — giving you early warning if credentials expire or network access is lost.

Error Handling

SecretVault normalizes provider-specific exceptions into a clean hierarchy so you never have to catch Amazon.Runtime.AmazonServiceException or Azure.RequestFailedException in your business logic:

try
{
    var secret = await secrets.GetSecretAsync("my-secret");
}
catch (SecretNotFoundException ex)
{
    // Secret doesn't exist in the provider
    logger.LogWarning("Secret {Name} not found", ex.SecretName);
}
catch (SecretAccessDeniedException)
{
    // IAM / RBAC permissions issue
    logger.LogError("Access denied — check your IAM role");
}
catch (SecretProviderUnavailableException)
{
    // Network or service outage
    logger.LogCritical("Secret provider is down");
}

CI/CD — Automated NuGet Publishing

The repo ships with two GitHub Actions workflows out of the box.

ci.yml — runs on every push and PR:

  • Restores dependencies
  • Builds in Release mode
  • Runs all tests with code coverage

publish.yml — triggered by a version tag:

git tag v1.0.0
git push origin v1.0.0

This automatically:

  1. Runs the full test suite
  2. Packs all 6 NuGet packages with the correct version
  3. Pushes them to nuget.org
  4. Creates a GitHub Release with auto-generated changelog

Zero manual steps after tagging.

Design Decisions Worth Knowing

Why separate packages instead of one big one?
If you only use AWS, you shouldn't have the Azure and GCP SDKs in your dependency tree. Each provider package is independent — install exactly what you need.

Why not just use IConfiguration directly?
IConfiguration is great for startup binding, but it doesn't support on-demand fetching, rotation, versioning, or cache invalidation. SecretVault gives you both — startup binding and a runtime API through the same abstraction.

Why .NET 9 only?
To keep the codebase lean and take advantage of modern C# features (primary constructors, collection expressions, nullable reference types). A netstandard2.0 target may come in a future release if there's demand.

Get Started

# Clone
git clone https://github.com/YourUsername/SecretVault.git
cd SecretVault

# Build
dotnet build SecretVault.sln

# Test
dotnet test SecretVault.sln

Or grab it straight from NuGet:

dotnet add package SecretVault.Core
dotnet add package SecretVault.Aws        # or .Azure / .HashiCorp / .Gcp
dotnet add package SecretVault.Extensions.Hosting

What's Next

A few things on the roadmap:

  • SecretVault.Local — a file/environment variable provider for local development
  • Automatic rotation scheduling — background IHostedService that rotates on a cron
  • Encryption at rest layer — wrap any provider with AES encryption before storing
  • .NET Standard 2.0 target — for broader framework compatibility

PRs and issues welcome. If you're using a provider not listed here, opening an issue with your use case is the fastest path to getting it supported.

Built with .NET 9, xUnit, FluentAssertions, and NSubstitute. MIT licensed.

⭐ If SecretVault saves you some boilerplate, a star on GitHub goes a long way.

View on GitHub · NuGet Packages

DE
Source

This article was originally published by DEV Community and written by Naimul Karim.

Read original article on DEV Community
Back to Discover

Reading List