SDK .NET

Carga secretos directamente en una aplicación .NET al iniciarse.

.NET SDK

¿Cómo cargas secretos de AWS SSM Parameter Store o de Azure Key Vault en una aplicación .NET sin un archivo .env? Añade el paquete NuGet de Envilder y llama a AddEnvilder en tu configuración: los secretos que lista envilder.json se resuelven al arrancar y se leen como cualquier otra clave de configuración.

Requisitos previos

El paquete apunta a .NET Standard 2.0, así que funciona con cualquier versión de .NET con soporte (las pruebas del SDK se ejecutan en .NET 10, la LTS actual). Cada proveedor usa su cadena de credenciales por defecto:

  • AWS: la cadena de credenciales por defecto de AWS (variables de entorno, archivo de credenciales compartido, SSO o un rol IAM). Define "profile" en $config para usar un perfil con nombre.
  • Azure: DefaultAzureCredential (az login, identidad administrada o variables de entorno).

Política IAM mínima para AWS SSM

El SDK solo lee parámetros, así que basta con ssm:GetParameter sobre tu prefijo de ruta. Sustituye la región, el ID de cuenta y el prefijo:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": "ssm:GetParameter",
    "Resource": "arn:aws:ssm:eu-west-1:123456789012:parameter/myapp/prod/*"
  }]
}

Los valores se leen con descifrado. Los parámetros SecureString cifrados con la clave aws/ssm por defecto no necesitan nada más; si usan una clave KMS gestionada por ti, permite también kms:Decrypt sobre esa clave.

Rol RBAC mínimo para Azure Key Vault

Para leer secretos solo hace falta el rol Key Vault Secrets User. Asígnalo al ámbito del almacén, no de la suscripción:

az role assignment create \
  --role "Key Vault Secrets User" \
  --assignee {APP_ID} \
  --scope /subscriptions/{SUB}/resourceGroups/{RG}/providers/Microsoft.KeyVault/vaults/{VAULT}

Si el almacén todavía usa políticas de acceso en lugar de RBAC, concede solo el permiso get de secretos:

az keyvault set-policy \
  --name {VAULT_NAME} \
  --object-id {OBJECT_ID} \
  --secret-permissions get

Instalación

dotnet add package Envilder

El archivo de mapeo

El mismo envilder.json sirve para los ejemplos siguientes. Las claves se convierten en claves de configuración o nombres de variables de entorno; los valores son rutas de parámetros en SSM:

envilder.jsonjson
{
  "$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"
}

Para Key Vault, define el proveedor y la URL del almacén en $config. Los valores son nombres de secretos de Key Vault:

envilder.jsonjson
{
  "$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"
}

El SDK de .NET envía los nombres a Key Vault tal como los escribes, salvo que las barras se convierten en guiones, igual que la CLI: una ruta como /myapp/prod/db-password se lee como myapp-prod-db-password. Cualquier otro carácter que Key Vault rechace (por ejemplo _ o .) falla con su propio error.

Una línea: resolver + inyectar

Resuelve secretos del archivo de mapeo e inyéctalos en Environment en una sola llamada:

using Envilder;

// Resolve secrets and inject into Environment
Env.Load("envilder.json");

var dbPassword = Environment.GetEnvironmentVariable("DB_PASSWORD");

Resolver sin inyectar

Obtén secretos como un diccionario sin modificar el entorno:

using Envilder;

var secrets = Env.ResolveFile("envilder.json");
var dbPassword = secrets["DB_PASSWORD"];

Constructor fluido con sobreescrituras

Sobreescribe la configuración del proveedor de forma programática con la API fluida:

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();

Carga basada en entorno

Enruta la carga de secretos según tu entorno actual. Cada entorno mapea a su archivo de secretos:

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
});

Validación de secretos

Validación opcional que asegura que todos los secretos resueltos tienen valores no vacíos:

using Envilder;

var secrets = Env.ResolveFile("envilder.json");
secrets.ValidateSecrets(); // throws SecretValidationException if any value is empty

Validación del contrato del fichero de mapeo

Comprueba, sin cargar nada y sin usar ni mostrar ningún valor, que el fichero de mapeo es válido y que cada secreto declarado existe con las credenciales actuales: la misma comprobación que envilder validate. Los errores de validación esperados se devuelven en el resultado en lugar de lanzarse; un fichero de mapeo inexistente sí lanza un error. Cada clave informa exists, missing, access_denied, authentication_failed o provider_error:

Consulta la referencia de envilder validate para los estados y los permisos que necesita la comprobación →

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}");
    }
}

Para que la carga falle, activa la validación del contrato en el builder. La comprobación se ejecuta una vez antes de resolver y el error nombra las claves que fallan, nunca sus valores. Está desactivada por defecto, así que una carga normal no hace ninguna llamada extra al proveedor. A diferencia de la validación de secretos, que revisa los valores resueltos, revisa el fichero de mapeo y las claves antes de resolver:

var secrets = await Env.FromMapFile("envilder.json")
    .WithContractValidation()
    .InjectAsync(); // throws ContractValidationException

Avanzado: control programático total

Analiza el archivo de mapeo, resuelve secretos e inyéctalos en las variables de entorno:

using Envilder;

var secrets = await Env.ResolveFileAsync("envilder.json");
EnvilderClient.InjectIntoEnvironment(secrets);

ASP.NET Core con IConfiguration

Llama a AddEnvilder justo después de CreateBuilder. Las claves se leen tal como se escriben en el archivo de mapeo, así que usa nombres de variable de entorno: el mismo archivo funciona también con la CLI y el resto de SDKs.

Program.cscsharp
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; } = "";
}

Al llamar al endpoint, la respuesta es:

{"dbConfigured":true,"stripeConfigured":true}

Los secretos se resuelven una sola vez, mientras se construye la configuración, con una petición por cada entrada del archivo. Los valores no se actualizan mientras la aplicación se ejecuta: reiníciala para recoger un secreto rotado. Un secreto que no existe se omite sin error, así que comprueba las claves sin las que tu aplicación no puede arrancar.

Inyección de dependencias

AddEnvilder sobre IServiceCollection registra un EnvilderClient y el archivo de mapeo analizado como singletons, para el código que resuelve secretos bajo demanda:

builder.Services.AddEnvilder("envilder.json");

app.MapGet("/count", async (EnvilderClient client, ParsedMapFile map) =>
    (await client.ResolveSecretsAsync(map)).Count);

Worker o aplicación de consola sin DI

No hace falta ningún host. Resuelve el archivo con Env, valídalo y usa el diccionario directamente:

envilder.jsonjson
{
  "PAYMENTS_API_KEY": "/myapp/prod/payments/api-key",
  "PAYMENTS_WEBHOOK_SECRET": "/myapp/prod/payments/webhook-secret"
}
Program.cscsharp
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;
}

Salida cuando los dos secretos se resuelven:

Resolved 2 secrets

En un Worker Service, llama a Env.Load antes de crear el host. Los valores pasan a ser variables de entorno del proceso, que la configuración del host ya lee, así que los guiones bajos dobles funcionan como separadores de sección (Payments__ApiKey se lee como Payments:ApiKey).

Program.cscsharp
using Envilder;

Env.Load("envilder.json");

var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();
builder.Build().Run();

Combinación con appsettings.{Environment}.json

ASP.NET Core aplica las fuentes de configuración en orden y gana la última. El host por defecto las añade de menor a mayor prioridad:

  1. appsettings.json
  2. appsettings.{Environment}.json
  3. Secretos de usuario (solo en Development)
  4. Variables de entorno
  5. Argumentos de línea de comandos
  6. Envilder, cuando se añade después de CreateBuilder como en el ejemplo anterior

Como AddEnvilder se ejecuta después de CreateBuilder, sus secretos sobrescriben todas las fuentes por defecto, incluida la línea de comandos. Deja los valores por defecto que no son secretos en appsettings y haz que Envilder aporte solo secretos. Una clave que se omite porque su secreto no existe no borra nada: el valor de appsettings sigue vigente.

Para que las variables de entorno vuelvan a ganar en un despliegue concreto, añade esa fuente después de Envilder:

builder.Configuration
    .AddEnvilder("envilder.json")
    .AddEnvironmentVariables();

Para usar un archivo distinto por entorno, construye su nombre a partir del nombre del entorno:

builder.Configuration.AddEnvilder(
    $"envilder.{builder.Environment.EnvironmentName}.json");

Sin el SDK: envilder exec

Si la aplicación solo necesita los valores como variables de entorno, prescinde del paquete y deja que la CLI la arranque. Los secretos se pasan al proceso, no se escribe ningún archivo .env y el comando no se inicia si algún secreto no se puede resolver:

envilder exec -- dotnet run

El proveedor de variables de entorno de .NET convierte los guiones bajos dobles en separadores de sección, así que una clave como ConnectionStrings__Default se lee como ConnectionStrings:Default sin código adicional.

Referencia de envilder exec →

Errores habituales

Cada mensaje siguiente es el texto exacto que lanza el SDK o que devuelve el proveedor de nube.

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).

El token de seguridad o la sesión SSO ha caducado. Renueva las credenciales (aws sso login si usas SSO) y vuelve a ejecutar. La excepción original de AWS está en InnerException.

SsoSessionExpiredException

Your AWS SSO session for profile 'dev' has expired. Run 'aws sso login --profile dev' and re-run this command.

Ejecuta el comando que indica el mensaje. Sin perfil en $config, el mensaje es: Your AWS SSO session has expired. Run 'aws sso login' and re-run this command. La propiedad ProfileName contiene el perfil, o null.

Parámetro o secreto inexistente

No secrets were resolved.
The following secrets have empty or missing values: STRIPE_KEY

Un parámetro o secreto que no existe se omite sin error: la clave no está en el diccionario ni en IConfiguration. ValidateSecrets solo lanza la excepción cuando no se ha resuelto nada o hay un valor vacío, así que comprueba también las claves de las que dependes (por ejemplo con TryGetValue) y verifica la ruta en el archivo.

Permiso denegado

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 action

La identidad no tiene ssm:GetParameter sobre ese parámetro. Compara el ARN de Resource, región incluida, con la política anterior.

Caller is not authorized to perform action on resource.

Asigna Key Vault Secrets User sobre el almacén. Las asignaciones de rol pueden tardar unos minutos en propagarse.

Secreto no encontrado en Azure

El SDK envía cada valor tal cual, salvo que las barras se convierten en guiones: un nombre en forma de ruta como /myapp/prod/db-password se lee como myapp-prod-db-password, igual que en el CLI. Si el secreto sigue sin aparecer, comprueba que el vault tenga un secreto con ese nombre; los nombres que solo difieren en mayúsculas son el mismo secreto.

Errores de configuración

MensajeSolución
Map file not found.
Map file not found: envilder.json
La ruta es relativa al directorio de trabajo, no a la carpeta del proyecto. Copia el archivo al directorio de salida o pasa una ruta absoluta.
Vault URL must be provided for Azure Key Vault provider.
Añade "vaultUrl" a $config o pásalo con EnvilderOptions.
AWS profile cannot be used with Azure Key Vault provider.
Elimina "profile" de $config: solo se aplica a AWS.
Vault URL cannot be used with AWS SSM provider.
Elimina "vaultUrl" o define "provider" como "azure".
AWS profile 'dev' was not found in the credential store.
Comprueba el nombre en ~/.aws/credentials o ~/.aws/config, o elimina "profile" para usar la cadena de credenciales por defecto.