Reputation: 55
I am new to Azure and the Azure Key Vault. I've read a number of tutorials about how to setup my Web API to leverage the Azure Key Vault but after successfully storing a secret and setting up my Program.cs
file with the code below, I can't figure out what I'm supposed to do next to actually access the secret in my classes.
My original goal was to store the connection string for my database in Key Vault, then when the API starts up I would get the connection string and "store" it locally (in memory) to be used by my data access layer classes.
var builder = WebApplication.CreateBuilder(args);
var keyVaultEndpoint = new Uri(Environment.GetEnvironmentVariable("VaultUri"));
builder.Configuration.AddAzureKeyVault(keyVaultEndpoint,
new DefaultAzureCredential());
I have a class that inherits from DbContext
and was expecting to get the secret to set the class's connection string - ideally without going back to the key vault every time and pulling it locally (but not storing it in a file). I'm thinking this is just a gap on my side of how this flow is supposed to work.
Any help to get me on track would be great!
I can see that builder.Configuration
does include the secrets I'm after. Now I'm just struggling to get that information to a place that I can access from my DbContext classes without additional calls to Key Vault and without storing them in a local file.
Upvotes: 4
Views: 8045
Reputation: 16076
I want to add more about DefaultAzureCredential.
OP said he already saw the secrets in builder.Configuration
after builder.Configuration.AddAzureKeyVault(keyVaultEndpoint, new DefaultAzureCredential());
which is using DefaultAzureCredential to connect to Azure KeyVault like the official document introduced.
Certainly we can reach the secrets from the built-in configuration property like builder.Configuration.GetConnectionString("property_name")
but we still need to know if we are planning to host the app in Azure app service and wants to use managed identity, then we need to use code below instead.
builder.Configuration.AddAzureKeyVault(
new Uri("https://vaultName.vault.azure.net/"),
new DefaultAzureCredential(
new DefaultAzureCredentialOptions { ManagedIdentityClientId = "userManagedIdentityClientId" }//required when using user ManagedIdentity
));
If we don't prefer to use managed identity, or we want to host the app in a VM, then we need to set environment variables: AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_CLIENT_SECRET
to make sure the DefaultAzureCredential
could also work. Don't forget to update the access-control policy for managed identity or AAD application.
Upvotes: 1
Reputation: 8490
We are using a custom IKeyVaultSecretManager
to replace secret placeholders in our app settings files with key vault secrets.
A config value of the form ${secret-name}
will be replaced with the value of a key vault secret of the same name. This all happens in memory when the app settings file is loaded into a IConfiguration
instance.
The custom secret manager:
namespace Foo;
using Azure.Extensions.AspNetCore.Configuration.Secrets;
using Azure.Security.KeyVault.Secrets;
using Microsoft.Extensions.Configuration;
internal class KeyVaultSecretReplacer : KeyVaultSecretManager
{
private readonly Dictionary<string, string> _placeholders;
public KeyVaultSecretReplacer(IConfiguration config)
=> _placeholders = config
.AsEnumerable()
.Where(HasSecretPlaceholder)
.ToDictionary(
keySelector: GetSecretName,
elementSelector: GetSettingKey,
StringComparer.OrdinalIgnoreCase);
public override bool Load(SecretProperties secret) => _placeholders.ContainsKey(secret.Name);
public override Dictionary<string, string> GetData(IEnumerable<KeyVaultSecret> secrets)
{
var loadedSecrets = secrets.ToArray();
ValidateAllSecretsFound(loadedSecrets);
return base.GetData(loadedSecrets);
}
private void ValidateAllSecretsFound(IList<KeyVaultSecret> secrets)
{
var missing = _placeholders.Keys.Except(secrets.Select(s => s.Name)).ToArray();
if (!missing.Any())
{
return;
}
var vaultName = secrets.FirstOrDefault()?.Properties.VaultUri.Host.Replace(".vault.azure.net", "");
var notFoundMsg = string.Join(", ", missing.Select(s => $"'{s}' (setting '{_placeholders[s]}')"));
throw new InvalidOperationException(
$"Some secrets were not found in key vault{(vaultName is null ? "" : $" '{vaultName}'")}: {notFoundMsg}.");
}
public override string GetKey(KeyVaultSecret secret) => _placeholders[secret.Name];
internal static bool HasSecretPlaceholder(KeyValuePair<string, string?> c)
=> c.Value != null && c.Value.StartsWith("${") && c.Value.EndsWith("}");
private static string GetSecretName(KeyValuePair<string, string?> c) => c.Value![2..^1];
private static string GetSettingKey(KeyValuePair<string, string?> c) => c.Key;
}
Parts of the usage:
private static void AddKeyVaultSecretReplacer(IConfigurationBuilder configBuilder, IConfigurationRoot config)
{
var settings = config.GetSection(nameof(SecretReplacement))?.Get<SecretReplacementSettings>()
?? throw new InvalidOperationException($"Secret placeholders found in app settings but {nameof(SecretReplacement)} has not been configured.");
configBuilder.AddAzureKeyVault(
new Uri(settings.AzureKeyVaultUrl),
new DefaultAzureCredential(new DefaultAzureCredentialOptions { TenantId = settings.AzureTenantId }),
new KeyVaultSecretReplacer(config));
}
Upvotes: 0