.NET SDK
Load secrets directly into a .NET application at startup.
.NET SDK
How do you load AWS SSM Parameter Store or Azure Key Vault secrets into a .NET application without a .env file? Add the Envilder NuGet package and call AddEnvilder on your configuration: the secrets listed in envilder.json are resolved at startup and read like any other configuration key.
Prerequisites
The package targets .NET Standard 2.0, so it runs on any in-support .NET release (its tests run on .NET 10, the current LTS). Each provider uses its own default credential chain:
- AWS: the default AWS credential chain (environment variables, shared credentials file, SSO or an IAM role). Set "profile" in $config to use a named profile.
- Azure: DefaultAzureCredential (az login, managed identity or environment variables).
Minimum IAM policy for AWS SSM
The SDK only reads parameters, so ssm:GetParameter on your path prefix is enough. Replace the region, account ID and prefix:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "ssm:GetParameter",
"Resource": "arn:aws:ssm:eu-west-1:123456789012:parameter/myapp/prod/*"
}]
}Values are read with decryption. SecureString parameters encrypted with the default aws/ssm key need nothing more; if they use a customer-managed KMS key, also allow kms:Decrypt on that key.
Minimum RBAC role for Azure Key Vault
Reading secrets only needs the Key Vault Secrets User role. Scope it to the vault, not the subscription:
az role assignment create \
--role "Key Vault Secrets User" \
--assignee {APP_ID} \
--scope /subscriptions/{SUB}/resourceGroups/{RG}/providers/Microsoft.KeyVault/vaults/{VAULT}If the vault still uses access policies instead of RBAC, grant only the get secret permission:
az keyvault set-policy \
--name {VAULT_NAME} \
--object-id {OBJECT_ID} \
--secret-permissions getInstall
dotnet add package EnvilderThe map file
The same envilder.json drives the examples below. Keys become configuration keys or environment variable names; values are parameter paths in SSM:
{
"$schema": "https://envilder.com/schema/map-file.v1.json",
"$config": { "provider": "aws" },
"DB_CONNECTION": "/myapp/prod/db-connection",
"STRIPE_API_KEY": "/myapp/prod/stripe-key"
}For Key Vault, set the provider and the vault URL in $config. Values are Key Vault secret names:
{
"$schema": "https://envilder.com/schema/map-file.v1.json",
"$config": {
"provider": "azure",
"vaultUrl": "https://my-vault.vault.azure.net"
},
"DB_CONNECTION": "myapp-prod-db-connection",
"STRIPE_API_KEY": "myapp-prod-stripe-key"
}The .NET SDK sends names to Key Vault as written, except that slashes become hyphens, like the CLI: a path such as /myapp/prod/db-password is read as myapp-prod-db-password. Any other character Key Vault rejects (for example _ or .) fails with its own error.
One-liner: resolve + inject
Resolve secrets from the map file and inject into Environment in a single call:
using Envilder;
// Resolve secrets and inject into Environment
Env.Load("envilder.json");
var dbPassword = Environment.GetEnvironmentVariable("DB_PASSWORD");Resolve without injecting
Get secrets as a dictionary without modifying the environment:
using Envilder;
var secrets = Env.ResolveFile("envilder.json");
var dbPassword = secrets["DB_PASSWORD"];Fluent builder with overrides
Override provider settings programmatically using the fluent API:
using Envilder;
var secrets = Env.FromMapFile("envilder.json")
.WithProvider(SecretProviderType.Azure)
.WithVaultUrl("https://my-vault.vault.azure.net")
.Resolve();
// Or inject directly
Env.FromMapFile("envilder.json")
.WithProfile("staging")
.Inject();Environment-based loading
Route secret loading based on your current environment. Each environment maps to its own secrets file:
using Envilder;
var env = Environment.GetEnvironmentVariable("APP_ENV") ?? "development";
Env.Load(env, new Dictionary<string, string?>
{
["production"] = "prod-secrets.json",
["development"] = "dev-secrets.json",
["test"] = null, // no secrets loaded
});Secret validation
Opt-in validation ensures all resolved secrets have non-empty values:
using Envilder;
var secrets = Env.ResolveFile("envilder.json");
secrets.ValidateSecrets(); // throws SecretValidationException if any value is emptyMap file contract validation
Check, without loading anything and without using or printing any value, that the map file is valid and that every declared secret exists with the current credentials: the same check as envilder validate. Expected validation failures are returned in the result instead of thrown; a missing map file still throws. Each key reports exists, missing, access_denied, authentication_failed or provider_error:
See the envilder validate reference for the statuses and the permissions the check needs →
using Envilder;
var result = await Env.FromMapFile("envilder.json").ValidateContractAsync();
if (!result.IsValid)
{
foreach (var error in result.MapFileErrors)
{
Console.WriteLine($"{error.Key}: {error.Message}");
}
foreach (var key in result.Keys.Where(k => k.Status != SecretExistenceStatus.Exists))
{
Console.WriteLine($"{key.Variable} ({key.Path}): {key.Status}");
}
}To fail the load instead, enable contract validation on the builder. The check runs once before resolving and the error names the failing keys, never their values. It is off by default, so a normal load makes no extra provider call. Unlike secret validation, which checks resolved values, it checks the map file and the keys before resolving:
var secrets = await Env.FromMapFile("envilder.json")
.WithContractValidation()
.InjectAsync(); // throws ContractValidationExceptionAdvanced: full programmatic control
Parse the map file, resolve secrets, and inject them into environment variables:
using Envilder;
var secrets = await Env.ResolveFileAsync("envilder.json");
EnvilderClient.InjectIntoEnvironment(secrets);ASP.NET Core with IConfiguration
Call AddEnvilder right after CreateBuilder. Keys are read exactly as written in the map file, so use environment-variable-style names: the same file then also works with the CLI and the other SDKs.
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddEnvilder("envilder.json");
builder.Services.Configure<StripeOptions>(options =>
options.ApiKey = builder.Configuration["STRIPE_API_KEY"] ?? "");
var app = builder.Build();
app.MapGet("/", (IConfiguration config) => new
{
dbConfigured = config["DB_CONNECTION"] is not null,
stripeConfigured = config["STRIPE_API_KEY"] is not null,
});
app.Run();
public class StripeOptions
{
public string ApiKey { get; set; } = "";
}Calling the endpoint returns:
{"dbConfigured":true,"stripeConfigured":true}Secrets are resolved once, while the configuration is built, with one request per entry in the map. Values do not refresh while the app runs: restart it to pick up a rotated secret. A secret that does not exist is skipped without an error, so check the keys your app cannot start without.
Dependency injection
AddEnvilder on IServiceCollection registers an EnvilderClient and the parsed map file as singletons, for code that resolves secrets on demand:
builder.Services.AddEnvilder("envilder.json");
app.MapGet("/count", async (EnvilderClient client, ParsedMapFile map) =>
(await client.ResolveSecretsAsync(map)).Count);Worker or console app without DI
No host is needed. Resolve the map with Env, validate it and use the dictionary directly:
{
"PAYMENTS_API_KEY": "/myapp/prod/payments/api-key",
"PAYMENTS_WEBHOOK_SECRET": "/myapp/prod/payments/webhook-secret"
}using Envilder;
try
{
var secrets = await Env.ResolveFileAsync("envilder.json");
string[] required = { "PAYMENTS_API_KEY", "PAYMENTS_WEBHOOK_SECRET" };
var missing = required.Where(key => !secrets.ContainsKey(key)).ToList();
if (missing.Count > 0)
{
Console.Error.WriteLine($"Missing secrets: {string.Join(", ", missing)}");
return 1;
}
secrets.ValidateSecrets(); // rejects empty values
Console.WriteLine($"Resolved {secrets.Count} secrets");
return 0;
}
catch (Exception ex) when (ex is SsoSessionExpiredException
or ExpiredCredentialsException)
{
Console.Error.WriteLine(ex.Message);
return 1;
}Output when both secrets resolve:
Resolved 2 secretsIn a Worker Service, call Env.Load before creating the host. The values become process environment variables, which the host configuration already reads, so double underscores work as section separators (Payments__ApiKey is read as Payments:ApiKey).
using Envilder;
Env.Load("envilder.json");
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();
builder.Build().Run();Combining with appsettings.{Environment}.json
ASP.NET Core applies configuration sources in order and the last one wins. The default host adds them from lowest to highest priority:
appsettings.jsonappsettings.{Environment}.json- User secrets (Development only)
- Environment variables
- Command-line arguments
- Envilder, when added after CreateBuilder as in the example above
Because AddEnvilder runs after CreateBuilder, its secrets override every default source, including the command line. Keep non-secret defaults in appsettings and let Envilder supply only secrets. A key skipped because its secret does not exist does not erase anything: the appsettings value stays in effect.
To let environment variables win again in a given deployment, add that source after Envilder:
builder.Configuration
.AddEnvilder("envilder.json")
.AddEnvironmentVariables();To use a different map per environment, build the file name from the environment name:
builder.Configuration.AddEnvilder(
$"envilder.{builder.Environment.EnvironmentName}.json");Without the SDK: envilder exec
If the app only needs the values as environment variables, skip the package and let the CLI start it. The secrets are passed to the process, no .env file is written, and the command does not start if a secret cannot be resolved:
envilder exec -- dotnet runThe .NET environment variable provider turns double underscores into section separators, so a map key such as ConnectionStrings__Default is read as ConnectionStrings:Default with no extra code.
Common errors
Each message below is the exact text raised by the SDK or returned by the cloud provider.
ExpiredCredentialsException
AWS credentials are expired or invalid. Your security token or SSO session may have expired. Refresh your credentials and retry (for SSO, run: aws sso login).The security token or SSO session has expired. Refresh the credentials (aws sso login for SSO) and run again. The original AWS exception is in InnerException.
SsoSessionExpiredException
Your AWS SSO session for profile 'dev' has expired. Run 'aws sso login --profile dev' and re-run this command.Run the command in the message. Without a profile in $config the message reads: Your AWS SSO session has expired. Run 'aws sso login' and re-run this command. The ProfileName property holds the profile, or null.
Missing parameter or secret
No secrets were resolved.
The following secrets have empty or missing values: STRIPE_KEYA parameter or secret that does not exist is skipped without an error: the key is absent from the dictionary and from IConfiguration. ValidateSecrets throws only when nothing resolved or a value is empty, so also check the keys you depend on (for example with TryGetValue) and verify the path in the map.
Access denied
User: arn:aws:sts::123456789012:assumed-role/myapp-prod/session is not authorized to perform: ssm:GetParameter on resource: arn:aws:ssm:eu-west-1:123456789012:parameter/myapp/prod/stripe-key because no identity-based policy allows the ssm:GetParameter actionThe identity lacks ssm:GetParameter on that parameter. Compare the Resource ARN, including the region, with the policy above.
Caller is not authorized to perform action on resource.Assign Key Vault Secrets User on the vault. Role assignments can take a few minutes to propagate.
Secret not found on Azure
The SDK sends each value as written, except that slashes become hyphens: a path-style name such as /myapp/prod/db-password is read as myapp-prod-db-password, like the CLI does. If the secret is still not found, check that the vault holds a secret with that name; names that differ only in case are the same secret.
Configuration errors
| Message | Fix |
|---|---|
Map file not found.Map file not found: envilder.json | The path is relative to the working directory, not the project folder. Copy the file to the output directory or pass an absolute path. |
Vault URL must be provided for Azure Key Vault provider. | Add "vaultUrl" to $config, or pass it with EnvilderOptions. |
AWS profile cannot be used with Azure Key Vault provider. | Remove "profile" from $config: it only applies to AWS. |
Vault URL cannot be used with AWS SSM provider. | Remove "vaultUrl", or set "provider" to "azure". |
AWS profile 'dev' was not found in the credential store. | Check the name in ~/.aws/credentials or ~/.aws/config, or remove "profile" to use the default credential chain. |