Skip to content

Instantly share code, notes, and snippets.

@robertjf
Last active August 14, 2021 10:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save robertjf/7d9eb123cc238d818c4324d2c7704d13 to your computer and use it in GitHub Desktop.
Save robertjf/7d9eb123cc238d818c4324d2c7704d13 to your computer and use it in GitHub Desktop.
Enhanced configuration management with Azure Key Vault - sample code for the blog post found at https://youritteam.com.au/blog/enhanced-configuration-management-with-azure-key-vault
{
"ConnectionString": "Server=tcp:127.0.0.1,5433;Database=IdentityDb;User Id=sa;Password=Pass@word;",
"Serilog": {
},
"Vault": {
"Enable": false
}
}
using Microsoft.AspNetCore.Hosting;
using Microsoft.Azure.KeyVault;
using Microsoft.Azure.Services.AppAuthentication;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Identity.Client;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using Serilog;
using System;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
namespace Services.KeyVault
{
public static class ConfigurationExtensions
{
/// <summary>
/// Configures and adds Azure KeyVault Configuration extensions
/// </summary>
/// <param name="builder"></param>
/// <param name="context"></param>
/// <param name="logger"></param>
/// <returns></returns>
public static IConfigurationBuilder UseAzureKeyVault(this IConfigurationBuilder builder, WebHostBuilderContext context, ILogger logger, string configurationSection = "Vault")
{
var config = builder.Build();
var options = KeyVaultSettings.GetFromConfigurationRoot(config, configurationSection);
if (!options.Enable)
{
logger.Information("Key Vault configuration Disabled");
return builder;
}
// Get details of the application to use for retrieving our configuration secrets
var assName = Assembly.GetEntryAssembly().GetName();
var appName = assName.Name.Replace(".", string.Empty);
var appVersion = assName.Version.ToString().Replace(".", string.Empty);
KeyVaultClient keyVaultClient = null;
if (context.HostingEnvironment.IsProduction())
{
logger.Information("Configuring access to Key Vault in Production on {Vault} using KeyVaultClient with Token Callback", options.VaultUrl);
var azureServiceTokenProvider = new AzureServiceTokenProvider();
keyVaultClient = new KeyVaultClient(
new KeyVaultClient.AuthenticationCallback(
azureServiceTokenProvider.KeyVaultTokenCallback));
}
else if (options.UseClientSecret)
{
logger.Information("Configuring access to Key Vault on {Vault} with Client Id and Secret.", options.VaultUrl);
keyVaultClient = new KeyVaultClient(async (authority, resource, scope) =>
{
var confidentialClientApplication = ConfidentialClientApplicationBuilder
.Create(options.ClientId)
.WithClientSecret(options.ClientSecret)
.WithAuthority(authority)
.Build();
var authenticationResult = await confidentialClientApplication
.AcquireTokenForClient(new string[] { "https://vault.azure.net/.default" })
.ExecuteAsync();
return authenticationResult.AccessToken;
});
}
else
{
logger.Warning("Unable to configure access to Key Vault on {Vault}", options.VaultUrl);
return builder;
}
var prefixer = new PrefixKeyVaultSecretManager(appName, appVersion, logger, keyVaultClient, options.VaultUrl);
builder.AddAzureKeyVault(options.VaultUrl, keyVaultClient, prefixer);
return builder;
}
}
}
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Text;
namespace Service.KeyVault
{
public sealed class KeyVaultSettings
{
internal static KeyVaultSettings GetFromConfigurationRoot(IConfigurationRoot config, string configurationSection = "Vault")
{
return new KeyVaultSettings
{
Enable = config.GetValue($"{configurationSection}:Enable", false),
ClientId = config[$"{configurationSection}:ClientId"],
ClientSecret = config[$"{configurationSection}:ClientSecret"]
};
}
public bool Enable { get; set; }
public string ClientId { get; set; }
public string ClientSecret { get; set; }
public bool UseClientSecret => !string.IsNullOrWhiteSpace(ClientId) && !string.IsNullOrWhiteSpace(ClientSecret);
public string VaultUrl => $"https://{Name}.vault.azure.net/";
}
}
using Microsoft.Azure.KeyVault;
using Microsoft.Azure.KeyVault.Models;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.AzureKeyVault;
using Microsoft.Rest.Azure;
using Serilog;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Services.KeyVault
{
public class PrefixKeyVaultSecretManager : IKeyVaultSecretManager
{
private readonly string _appPrefix;
private readonly string _versionPrefix;
private readonly bool _enableGlobalSecrets;
private const string GlobalSecretPrefix = "g-";
private const string KeyVaultConfigurationDelimiter = "--";
private List<Tuple<string, KeyType>> _secretKeys = null;
private readonly KeyVaultClient _client;
private readonly ILogger _logger;
private readonly string _keyVaultUrl;
/// <summary>
/// Creates a new PrefixKeyVaultSecretManager optionally allowing Global Secrets
/// </summary>
/// <remarks>A Global Secret begins with a single '-' in Azure KeyVault.
/// The '-' is removed when importing into configuration</remarks>
/// <param name="appPrefix"></param>
/// <param name="versionPrefix">Allows retrieving versioned keys in preference to the base app prefix</param>
/// <param name="enableGlobalSecrets"></param>
public PrefixKeyVaultSecretManager(string appPrefix, string versionPrefix, ILogger logger, KeyVaultClient client, string keyVaultUrl, bool enableGlobalSecrets = true)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_client = client ?? throw new ArgumentNullException(nameof(client));
_keyVaultUrl = keyVaultUrl ?? throw new ArgumentNullException(nameof(keyVaultUrl));
_appPrefix = $"{appPrefix}-";
_versionPrefix = $"{appPrefix}-{versionPrefix}";
_enableGlobalSecrets = enableGlobalSecrets;
if (enableGlobalSecrets)
logger.Information("Global Secrets Enabled");
}
public bool Load(SecretItem secret)
{
var secretKey = GetBaseKey(secret.Identifier.Name);
_logger.Debug("Base key for {secret}: {@baseKey}", secret.Identifier.Name, secretKey);
switch (secretKey.Item2)
{
case KeyType.Versioned:
return true;
case KeyType.Prefixed:
// true if we don't have a versioned instance.
return !FindRelatedKeys(secret).Any(s => s.Item2 == KeyType.Versioned);
case KeyType.Global:
// true if we don't have a prefixed or versioned instance.
return !FindRelatedKeys(secret).Any(s => s.Item2 == KeyType.Versioned || s.Item2 == KeyType.Prefixed);
}
return false;
}
/// <summary>
/// Gets a list of other secret keys that are related to the passed in secret
/// </summary>
/// <param name="secret"></param>
/// <returns></returns>
private IEnumerable<Tuple<string, KeyType>> FindRelatedKeys(SecretItem secret)
{
if (_secretKeys == null)
{
_secretKeys = new List<Tuple<string, KeyType>>();
string pageLink = null;
do
{
IPage<SecretItem> secrets;
if (pageLink == null)
{
secrets = _client.GetSecretsAsync(_keyVaultUrl).Result;
}
else
{
secrets = _client.GetSecretsNextAsync(pageLink).Result;
}
pageLink = secrets.NextPageLink;
foreach (var s in secrets)
{
var key = GetBaseKey(s.Identifier.Name);
if (!_secretKeys.Any(k => k.Item1 == key.Item1 && k.Item2 == key.Item2))
{
_secretKeys.Add(key);
}
}
if (!secrets.Any())
{
break;
}
} while (!string.IsNullOrWhiteSpace(pageLink));
}
var baseKey = GetBaseKey(secret.Identifier.Name);
return _secretKeys.Where(s => s.Item1 != baseKey.Item1 && s.Item2 != baseKey.Item2);
}
private Tuple<string, KeyType> GetBaseKey(string secretKey)
{
var type = KeyType.Unknown;
var key = secretKey;
if (secretKey.StartsWith(_versionPrefix, StringComparison.InvariantCultureIgnoreCase))
{
_logger.Debug("Found key using the Version Prefix {VersionPrefix}", _versionPrefix);
key = secretKey.Substring(_versionPrefix.Length);
type = KeyType.Versioned;
}
else if (secretKey.StartsWith(_appPrefix, StringComparison.InvariantCultureIgnoreCase))
{
_logger.Debug("Found key using the App Prefix {AppPrefix}", _appPrefix);
key = secretKey.Substring(_appPrefix.Length);
type = KeyType.Prefixed;
}
else if (_enableGlobalSecrets && secretKey.StartsWith(GlobalSecretPrefix, StringComparison.InvariantCultureIgnoreCase))
{
_logger.Debug("Found key using the Global Secret Prefix {GlobalSecretPrefix}", GlobalSecretPrefix);
key = secretKey.Substring(GlobalSecretPrefix.Length);
type = KeyType.Global;
}
_logger.Debug("Returning key {key} => {replacementKey} of type {type}", key,
key.Replace(KeyVaultConfigurationDelimiter, ConfigurationPath.KeyDelimiter),
type);
return new Tuple<string, KeyType>(key, type);
}
public string GetKey(SecretBundle secret)
{
var key = GetBaseKey(secret.SecretIdentifier.Name).Item1;
return key.Replace(KeyVaultConfigurationDelimiter, ConfigurationPath.KeyDelimiter);
}
}
}
public static IWebHost BuildWebHost(string[] args)
{
return WebHost.CreateDefaultBuilder(args)
.CaptureStartupErrors(false)
.UseStartup<Startup>()
.UseContentRoot(Directory.GetCurrentDirectory())
.ConfigureAppConfiguration((context, config) => {
config.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddEnvironmentVariables()
.UseAzureKeyVault(context, Log.Logger)
.AddJsonFile(Path.Combine("Configuration", "configuration.json"));
})
.UseApplicationInsights()
.UseSerilog()
.Build();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment