From e05e071c0f8aa6ba43256096f799ed3edb55b541 Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Tue, 16 Jul 2019 14:56:29 -0500 Subject: [PATCH 1/6] Update AKV config provider to 3.0 Updates Updates Updates Updates --- .../security/key-vault-configuration.md | 58 ++++++++---- .../sample_snapshot/Program.cs | 40 -------- .../sample_snapshot/Startup.cs | 64 ------------- .../2.x/SampleApp}/Markup.cs | 0 .../2.x/SampleApp}/Program.cs | 4 +- .../2.x/SampleApp}/README.md | 0 .../2.x/SampleApp}/SampleApp.csproj | 1 - .../2.x/SampleApp}/Startup.cs | 0 .../2.x/SampleApp}/appsettings.json | 0 .../samples/3.x/SampleApp/Markup.cs | 47 ++++++++++ .../samples/3.x/SampleApp/Program.cs | 93 +++++++++++++++++++ .../samples/3.x/SampleApp/README.md | 10 ++ .../samples/3.x/SampleApp/SampleApp.csproj | 13 +++ .../samples/3.x/SampleApp/Startup.cs | 31 +++++++ .../samples/3.x/SampleApp/appsettings.json | 5 + .../samples_snapshot/Program.cs | 5 + .../samples_snapshot/Startup.cs | 27 ++++++ 17 files changed, 274 insertions(+), 124 deletions(-) delete mode 100644 aspnetcore/security/key-vault-configuration/sample_snapshot/Program.cs delete mode 100644 aspnetcore/security/key-vault-configuration/sample_snapshot/Startup.cs rename aspnetcore/security/key-vault-configuration/{sample => samples/2.x/SampleApp}/Markup.cs (100%) rename aspnetcore/security/key-vault-configuration/{sample => samples/2.x/SampleApp}/Program.cs (96%) rename aspnetcore/security/key-vault-configuration/{sample => samples/2.x/SampleApp}/README.md (100%) rename aspnetcore/security/key-vault-configuration/{sample => samples/2.x/SampleApp}/SampleApp.csproj (83%) rename aspnetcore/security/key-vault-configuration/{sample => samples/2.x/SampleApp}/Startup.cs (100%) rename aspnetcore/security/key-vault-configuration/{sample => samples/2.x/SampleApp}/appsettings.json (100%) create mode 100644 aspnetcore/security/key-vault-configuration/samples/3.x/SampleApp/Markup.cs create mode 100644 aspnetcore/security/key-vault-configuration/samples/3.x/SampleApp/Program.cs create mode 100644 aspnetcore/security/key-vault-configuration/samples/3.x/SampleApp/README.md create mode 100644 aspnetcore/security/key-vault-configuration/samples/3.x/SampleApp/SampleApp.csproj create mode 100644 aspnetcore/security/key-vault-configuration/samples/3.x/SampleApp/Startup.cs create mode 100644 aspnetcore/security/key-vault-configuration/samples/3.x/SampleApp/appsettings.json create mode 100644 aspnetcore/security/key-vault-configuration/samples_snapshot/Program.cs create mode 100644 aspnetcore/security/key-vault-configuration/samples_snapshot/Startup.cs diff --git a/aspnetcore/security/key-vault-configuration.md b/aspnetcore/security/key-vault-configuration.md index 83a373d5bb85..7b9fbeb31a10 100644 --- a/aspnetcore/security/key-vault-configuration.md +++ b/aspnetcore/security/key-vault-configuration.md @@ -5,7 +5,7 @@ description: Learn how to use the Azure Key Vault Configuration Provider to conf monikerRange: '>= aspnetcore-2.1' ms.author: riande ms.custom: mvc -ms.date: 05/13/2019 +ms.date: 11/14/2019 uid: security/key-vault-configuration --- # Azure Key Vault Configuration Provider in ASP.NET Core @@ -17,18 +17,11 @@ This document explains how to use the [Microsoft Azure Key Vault](https://azure. * Controlling access to sensitive configuration data. * Meeting the requirement for FIPS 140-2 Level 2 validated Hardware Security Modules (HSM's) when storing configuration data. -This scenario is available for apps that target ASP.NET Core 2.1 or later. - -[View or download sample code](https://github.com/aspnet/AspNetCore.Docs/tree/master/aspnetcore/security/key-vault-configuration/sample) ([how to download](xref:index#how-to-download-a-sample)) +[View or download sample code](https://github.com/aspnet/AspNetCore.Docs/tree/master/aspnetcore/security/key-vault-configuration/samples) ([how to download](xref:index#how-to-download-a-sample)) ## Packages -To use the Azure Key Vault Configuration Provider, add a package reference to the [Microsoft.Extensions.Configuration.AzureKeyVault](https://www.nuget.org/packages/Microsoft.Extensions.Configuration.AzureKeyVault/) package. - -To adopt the [Managed identities for Azure resources](/azure/active-directory/managed-identities-azure-resources/overview) scenario, add a package reference to the [Microsoft.Azure.Services.AppAuthentication](https://www.nuget.org/packages/Microsoft.Azure.Services.AppAuthentication/) package. - -> [!NOTE] -> At the time of writing, the latest stable release of `Microsoft.Azure.Services.AppAuthentication`, version `1.0.3`, provides support for [system-assigned managed identities](/azure/active-directory/managed-identities-azure-resources/overview#how-does-the-managed-identities-for-azure-resources-work). Support for *user-assigned managed identities* is available in the `1.2.0-preview2` package. This topic demonstrates the use of system-managed identities, and the provided sample app uses version `1.0.3` of the `Microsoft.Azure.Services.AppAuthentication` package. +Add a package reference to the [Microsoft.Extensions.Configuration.AzureKeyVault](https://www.nuget.org/packages/Microsoft.Extensions.Configuration.AzureKeyVault/) package. ## Sample app @@ -126,9 +119,9 @@ The sample app uses an Application ID and X.509 certificate when the `#define` s 1. Navigate to **Key vaults** in the Azure portal. 1. Select the key vault that you created in the [Secret storage in the Production environment with Azure Key Vault](#secret-storage-in-the-production-environment-with-azure-key-vault) section. 1. Select **Access policies**. -1. Select **Add new**. -1. Select **Select principal** and select the registered app by name. Select the **Select** button. +1. Select **Add Access Policy**. 1. Open **Secret permissions** and provide the app with **Get** and **List** permissions. +1. Select **Select principal** and select the registered app by name. Select the **Select** button. 1. Select **OK**. 1. Select **Save**. 1. Deploy the app. @@ -142,7 +135,17 @@ The `Certificate` sample app obtains its configuration values from `IConfigurati The X.509 certificate is managed by the OS. The app calls `AddAzureKeyVault` with values supplied by the *appsettings.json* file: -[!code-csharp[](key-vault-configuration/sample/Program.cs?name=snippet1&highlight=20-23)] +::: moniker range=">= aspnetcore-3.0" + +[!code-csharp[](key-vault-configuration/samples/3.x/SampleApp/Program.cs?name=snippet1&highlight=20-23)] + +::: moniker-end + +::: moniker range="< aspnetcore-3.0" + +[!code-csharp[](key-vault-configuration/samples/2.x/SampleApp/Program.cs?name=snippet1&highlight=20-23)] + +::: moniker-end Example values: @@ -152,7 +155,17 @@ Example values: *appsettings.json*: -[!code-json[](key-vault-configuration/sample/appsettings.json)] +::: moniker range=">= aspnetcore-3.0" + +[!code-json[](key-vault-configuration/samples/3.x/SampleApp/appsettings.json?highlight=10-12)] + +::: moniker-end + +::: moniker range="< aspnetcore-3.0" + +[!code-json[](key-vault-configuration/samples/2.x/SampleApp/appsettings.json?highlight=10-12)] + +::: moniker-end When you run the app, a webpage shows the loaded secret values. In the Development environment, secret values load with the `_dev` suffix. In the Production environment, the values load with the `_prod` suffix. @@ -182,7 +195,17 @@ The sample app: * A new `KeyVaultClient` is created with the `AzureServiceTokenProvider` instance token callback. * The `KeyVaultClient` instance is used with a default implementation of `IKeyVaultSecretManager` that loads all secret values and replaces double-dashes (`--`) with colons (`:`) in key names. -[!code-csharp[](key-vault-configuration/sample/Program.cs?name=snippet2&highlight=13-21)] +::: moniker range=">= aspnetcore-3.0" + +[!code-csharp[](key-vault-configuration/samples/3.x/SampleApp/Program.cs?name=snippet2&highlight=13-21)] + +::: moniker-end + +::: moniker range="< aspnetcore-3.0" + +[!code-csharp[](key-vault-configuration/samples/2.x/SampleApp/Program.cs?name=snippet2&highlight=13-21)] + +::: moniker-end When you run the app, a webpage shows the loaded secret values. In the Development environment, secret values have the `_dev` suffix because they're provided by User Secrets. In the Production environment, the values load with the `_prod` suffix because they're provided by Azure Key Vault. @@ -199,11 +222,11 @@ In the following example, a secret is established in the key vault (and using th `AddAzureKeyVault` is called with a custom `IKeyVaultSecretManager`: -[!code-csharp[](key-vault-configuration/sample_snapshot/Program.cs?highlight=30-34)] +[!code-csharp[](key-vault-configuration/samples_snapshot/Program.cs)] The `IKeyVaultSecretManager` implementation reacts to the version prefixes of secrets to load the proper secret into configuration: -[!code-csharp[](key-vault-configuration/sample_snapshot/Startup.cs?name=snippet1)] +[!code-csharp[](key-vault-configuration/samples_snapshot/Startup.cs)] The `Load` method is called by a provider algorithm that iterates through the vault secrets to find the ones that have the version prefix. When a version prefix is found with `Load`, the algorithm uses the `GetKey` method to return the configuration name of the secret name. It strips off the version prefix from the secret's name and returns the rest of the secret name for loading into the app's configuration name-value pairs. @@ -313,6 +336,7 @@ When the app fails to load configuration using the provider, an error message is * In the key vault, the configuration data (name-value pair) is incorrectly named, missing, disabled, or expired. * The app has the wrong key vault name (`KeyVaultName`), Azure AD Application Id (`AzureADApplicationId`), or Azure AD certificate thumbprint (`AzureADCertThumbprint`). * The configuration key (name) is incorrect in the app for the value you're trying to load. +* When adding the access policy for the app to the key vault, the policy was created, but the **Save** button wasn't selected in the **Access policies** UI. ## Additional resources diff --git a/aspnetcore/security/key-vault-configuration/sample_snapshot/Program.cs b/aspnetcore/security/key-vault-configuration/sample_snapshot/Program.cs deleted file mode 100644 index afdc4eb33fb3..000000000000 --- a/aspnetcore/security/key-vault-configuration/sample_snapshot/Program.cs +++ /dev/null @@ -1,40 +0,0 @@ -// using System.Reflection; -// using Microsoft.Azure.KeyVault; -// using Microsoft.Azure.Services.AppAuthentication; -// using Microsoft.Extensions.Configuration; -// using Microsoft.Extensions.Configuration.AzureKeyVault; - -public static IWebHostBuilder CreateWebHostBuilder(string[] args) => - WebHost.CreateDefaultBuilder(args) - .ConfigureAppConfiguration((context, config) => - { - if (context.HostingEnvironment.IsProduction()) - { - // The appVersion obtains the app version (5.0.0.0), which - // is set in the project file and obtained from the entry - // assembly. The versionPrefix holds the version without - // dot notation for the PrefixKeyVaultSecretManager. - var appVersion = Assembly.GetEntryAssembly().GetName().Version.ToString(); - var versionPrefix = appVersion.Replace(".", string.Empty); - - var builtConfig = config.Build(); - - using (var store = new X509Store(StoreName.My, - StoreLocation.CurrentUser)) - { - store.Open(OpenFlags.ReadOnly); - var certs = store.Certificates - .Find(X509FindType.FindByThumbprint, - builtConfig["AzureADCertThumbprint"], false); - - config.AddAzureKeyVault( - $"https://{builtConfig["KeyVaultName"]}.vault.azure.net/", - builtConfig["AzureADApplicationId"], - certs.OfType().Single(), - new PrefixKeyVaultSecretManager(versionPrefix)); - - store.Close(); - } - } - }) - .UseStartup(); diff --git a/aspnetcore/security/key-vault-configuration/sample_snapshot/Startup.cs b/aspnetcore/security/key-vault-configuration/sample_snapshot/Startup.cs deleted file mode 100644 index e7831356ff5c..000000000000 --- a/aspnetcore/security/key-vault-configuration/sample_snapshot/Startup.cs +++ /dev/null @@ -1,64 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.Azure.KeyVault.Models; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Configuration.AzureKeyVault; -using System.Reflection; -using System.Text; - -namespace SampleApp -{ - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; set; } - - public void Configure(IApplicationBuilder app) - { - app.Run(async context => - { - var appVersion = Assembly.GetEntryAssembly().GetName().Version.ToString(); - var versionPrefix = appVersion.Replace(".", string.Empty); - var encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); - var document = string.Format(Markup.Text, versionPrefix, Configuration["AppSecret"]); - context.Response.ContentLength = encoding.GetByteCount(document); - context.Response.ContentType = "text/html"; - await context.Response.WriteAsync(document); - }); - } - } - - #region snippet1 - public class PrefixKeyVaultSecretManager : IKeyVaultSecretManager - { - private readonly string _prefix; - - public PrefixKeyVaultSecretManager(string prefix) - { - _prefix = $"{prefix}-"; - } - - public bool Load(SecretItem secret) - { - // Load a vault secret when its secret name starts with the - // prefix. Other secrets won't be loaded. - return secret.Identifier.Name.StartsWith(_prefix); - } - - public string GetKey(SecretBundle secret) - { - // Remove the prefix from the secret name and replace two - // dashes in any name with the KeyDelimiter, which is the - // delimiter used in configuration (usually a colon). Azure - // Key Vault doesn't allow a colon in secret names. - return secret.SecretIdentifier.Name - .Substring(_prefix.Length) - .Replace("--", ConfigurationPath.KeyDelimiter); - } - } - #endregion -} diff --git a/aspnetcore/security/key-vault-configuration/sample/Markup.cs b/aspnetcore/security/key-vault-configuration/samples/2.x/SampleApp/Markup.cs similarity index 100% rename from aspnetcore/security/key-vault-configuration/sample/Markup.cs rename to aspnetcore/security/key-vault-configuration/samples/2.x/SampleApp/Markup.cs diff --git a/aspnetcore/security/key-vault-configuration/sample/Program.cs b/aspnetcore/security/key-vault-configuration/samples/2.x/SampleApp/Program.cs similarity index 96% rename from aspnetcore/security/key-vault-configuration/sample/Program.cs rename to aspnetcore/security/key-vault-configuration/samples/2.x/SampleApp/Program.cs index 6a15997f79ed..dd4c43252894 100644 --- a/aspnetcore/security/key-vault-configuration/sample/Program.cs +++ b/aspnetcore/security/key-vault-configuration/samples/2.x/SampleApp/Program.cs @@ -26,6 +26,7 @@ public static void Main(string[] args) // using System.Linq; // using System.Security.Cryptography.X509Certificates; // using Microsoft.Extensions.Configuration; + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .ConfigureAppConfiguration((context, config) => @@ -34,8 +35,7 @@ public static IWebHostBuilder CreateWebHostBuilder(string[] args) => { var builtConfig = config.Build(); - using (var store = new X509Store(StoreName.My, - StoreLocation.CurrentUser)) + using (var store = new X509Store(StoreLocation.CurrentUser)) { store.Open(OpenFlags.ReadOnly); var certs = store.Certificates diff --git a/aspnetcore/security/key-vault-configuration/sample/README.md b/aspnetcore/security/key-vault-configuration/samples/2.x/SampleApp/README.md similarity index 100% rename from aspnetcore/security/key-vault-configuration/sample/README.md rename to aspnetcore/security/key-vault-configuration/samples/2.x/SampleApp/README.md diff --git a/aspnetcore/security/key-vault-configuration/sample/SampleApp.csproj b/aspnetcore/security/key-vault-configuration/samples/2.x/SampleApp/SampleApp.csproj similarity index 83% rename from aspnetcore/security/key-vault-configuration/sample/SampleApp.csproj rename to aspnetcore/security/key-vault-configuration/samples/2.x/SampleApp/SampleApp.csproj index 4bb5025c41d3..1748a157d8c8 100644 --- a/aspnetcore/security/key-vault-configuration/sample/SampleApp.csproj +++ b/aspnetcore/security/key-vault-configuration/samples/2.x/SampleApp/SampleApp.csproj @@ -8,7 +8,6 @@ - diff --git a/aspnetcore/security/key-vault-configuration/sample/Startup.cs b/aspnetcore/security/key-vault-configuration/samples/2.x/SampleApp/Startup.cs similarity index 100% rename from aspnetcore/security/key-vault-configuration/sample/Startup.cs rename to aspnetcore/security/key-vault-configuration/samples/2.x/SampleApp/Startup.cs diff --git a/aspnetcore/security/key-vault-configuration/sample/appsettings.json b/aspnetcore/security/key-vault-configuration/samples/2.x/SampleApp/appsettings.json similarity index 100% rename from aspnetcore/security/key-vault-configuration/sample/appsettings.json rename to aspnetcore/security/key-vault-configuration/samples/2.x/SampleApp/appsettings.json diff --git a/aspnetcore/security/key-vault-configuration/samples/3.x/SampleApp/Markup.cs b/aspnetcore/security/key-vault-configuration/samples/3.x/SampleApp/Markup.cs new file mode 100644 index 000000000000..0e3658941a86 --- /dev/null +++ b/aspnetcore/security/key-vault-configuration/samples/3.x/SampleApp/Markup.cs @@ -0,0 +1,47 @@ +namespace SampleApp +{ + internal static class Markup + { + internal const string Text = @" + + + + Key Vault Configuration Provider Sample + + + +

Key Vault Configuration Provider Sample

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
SecretName in Key VaultObtained from ConfigurationValue
SecretNameSecretNameConfiguration[""SecretName""]{0}
Section:SecretNameSection--SecretNameConfiguration[""Section:SecretName""]{1}
Configuration.GetSection(""Section"")[""SecretName""]{2}
+
+ + "; + } +} diff --git a/aspnetcore/security/key-vault-configuration/samples/3.x/SampleApp/Program.cs b/aspnetcore/security/key-vault-configuration/samples/3.x/SampleApp/Program.cs new file mode 100644 index 000000000000..207261133dfa --- /dev/null +++ b/aspnetcore/security/key-vault-configuration/samples/3.x/SampleApp/Program.cs @@ -0,0 +1,93 @@ +#define Certificate // Managed +// Change to 'Managed' to run the sample in Managed Identity configuration. +// For details, see the Azure Key Vault Configuration Provider topic: +// https://docs.microsoft.com/aspnet/core/security/key-vault-configuration + +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Azure.KeyVault; +using Microsoft.Azure.Services.AppAuthentication; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.AzureKeyVault; +using Microsoft.Extensions.Hosting; + +namespace SampleApp +{ + public static class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + +#if Certificate + #region snippet1 + // using System.Linq; + // using System.Security.Cryptography.X509Certificates; + // using Microsoft.Extensions.Configuration; + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureAppConfiguration((context, config) => + { + if (context.HostingEnvironment.IsProduction()) + { + var builtConfig = config.Build(); + + using (var store = new X509Store(StoreLocation.CurrentUser)) + { + store.Open(OpenFlags.ReadOnly); + var certs = store.Certificates + .Find(X509FindType.FindByThumbprint, + builtConfig["AzureADCertThumbprint"], false); + + config.AddAzureKeyVault( + $"https://{builtConfig["KeyVaultName"]}.vault.azure.net/", + builtConfig["AzureADApplicationId"], + certs.OfType().Single()); + + store.Close(); + } + } + }) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + #endregion +#endif + +#if Managed + #region snippet2 + // using Microsoft.Azure.KeyVault; + // using Microsoft.Azure.Services.AppAuthentication; + // using Microsoft.Extensions.Configuration.AzureKeyVault; + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureAppConfiguration((context, config) => + { + if (context.HostingEnvironment.IsProduction()) + { + var builtConfig = config.Build(); + + var azureServiceTokenProvider = new AzureServiceTokenProvider(); + var keyVaultClient = new KeyVaultClient( + new KeyVaultClient.AuthenticationCallback( + azureServiceTokenProvider.KeyVaultTokenCallback)); + + config.AddAzureKeyVault( + $"https://{builtConfig["KeyVaultName"]}.vault.azure.net/", + keyVaultClient, + new DefaultKeyVaultSecretManager()); + } + }) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + #endregion +#endif + } +} diff --git a/aspnetcore/security/key-vault-configuration/samples/3.x/SampleApp/README.md b/aspnetcore/security/key-vault-configuration/samples/3.x/SampleApp/README.md new file mode 100644 index 000000000000..1c5d18167bca --- /dev/null +++ b/aspnetcore/security/key-vault-configuration/samples/3.x/SampleApp/README.md @@ -0,0 +1,10 @@ +# Key Vault Configuration Provider Sample App + +This sample illustrates the use of the Azure Key Vault Configuration Provider. + +The sample runs in one of two modes determined by the `#define` statement at the top of the *Program.cs* file. For instructions, see [Preprocessor directives in sample code](https://docs.microsoft.com/aspnet/core#preprocessor-directives-in-sample-code): + +* `Certificate` – Demonstrates the use of an Azure Key Vault Client ID and X.509 certificate to access secrets stored in Azure Key Vault. This version of the sample can be run from any location, deployed to Azure App Service or any host capable of serving an ASP.NET Core app. +* `Managed` – Demonstrates how to use Azure's [Managed Service Identity](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/overview) to authenticate the app to Azure Key Vault with Azure AD authentication without credentials in the app's code or configuration. An Azure AD Client ID and Secret aren't required for the app to authenticate with Azure Key Vault. This sample must be deployed to Azure App Service to explore the Managed Identity scearnio. + +For more information, see [Azure Key Vault Configuration Provider](https://docs.microsoft.com/aspnet/core/security/key-vault-configuration). diff --git a/aspnetcore/security/key-vault-configuration/samples/3.x/SampleApp/SampleApp.csproj b/aspnetcore/security/key-vault-configuration/samples/3.x/SampleApp/SampleApp.csproj new file mode 100644 index 000000000000..0730747e9bf7 --- /dev/null +++ b/aspnetcore/security/key-vault-configuration/samples/3.x/SampleApp/SampleApp.csproj @@ -0,0 +1,13 @@ + + + + netcoreapp3.0 + + 06466f76-4a2b-4d64-94d9-72d76e179948 + + + + + + + diff --git a/aspnetcore/security/key-vault-configuration/samples/3.x/SampleApp/Startup.cs b/aspnetcore/security/key-vault-configuration/samples/3.x/SampleApp/Startup.cs new file mode 100644 index 000000000000..f511b16cefee --- /dev/null +++ b/aspnetcore/security/key-vault-configuration/samples/3.x/SampleApp/Startup.cs @@ -0,0 +1,31 @@ +using System.Text; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; + +namespace SampleApp +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; set; } + + public void Configure(IApplicationBuilder app) + { + app.Run(async context => + { + var encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + var document = string.Format(Markup.Text, Configuration["SecretName"], Configuration["Section:SecretName"], Configuration.GetSection("Section")["SecretName"]); + context.Response.ContentLength = encoding.GetByteCount(document); + context.Response.ContentType = "text/html"; + await context.Response.WriteAsync(document); + }); + } + } +} diff --git a/aspnetcore/security/key-vault-configuration/samples/3.x/SampleApp/appsettings.json b/aspnetcore/security/key-vault-configuration/samples/3.x/SampleApp/appsettings.json new file mode 100644 index 000000000000..ff2e451ee658 --- /dev/null +++ b/aspnetcore/security/key-vault-configuration/samples/3.x/SampleApp/appsettings.json @@ -0,0 +1,5 @@ +{ + "KeyVaultName": "Key Vault Name", + "AzureADApplicationId": "Azure AD Application ID", + "AzureADCertThumbprint": "Azure AD Certificate Thumbprint" +} diff --git a/aspnetcore/security/key-vault-configuration/samples_snapshot/Program.cs b/aspnetcore/security/key-vault-configuration/samples_snapshot/Program.cs new file mode 100644 index 000000000000..fb90fbad064b --- /dev/null +++ b/aspnetcore/security/key-vault-configuration/samples_snapshot/Program.cs @@ -0,0 +1,5 @@ +config.AddAzureKeyVault( + $"https://{builtConfig["KeyVaultName"]}.vault.azure.net/", + builtConfig["AzureADApplicationId"], + certs.OfType().Single(), + new PrefixKeyVaultSecretManager(versionPrefix)); diff --git a/aspnetcore/security/key-vault-configuration/samples_snapshot/Startup.cs b/aspnetcore/security/key-vault-configuration/samples_snapshot/Startup.cs new file mode 100644 index 000000000000..8171bfd5bea7 --- /dev/null +++ b/aspnetcore/security/key-vault-configuration/samples_snapshot/Startup.cs @@ -0,0 +1,27 @@ +public class PrefixKeyVaultSecretManager : IKeyVaultSecretManager +{ + private readonly string _prefix; + + public PrefixKeyVaultSecretManager(string prefix) + { + _prefix = $"{prefix}-"; + } + + public bool Load(SecretItem secret) + { + // Load a vault secret when its secret name starts with the + // prefix. Other secrets won't be loaded. + return secret.Identifier.Name.StartsWith(_prefix); + } + + public string GetKey(SecretBundle secret) + { + // Remove the prefix from the secret name and replace two + // dashes in any name with the KeyDelimiter, which is the + // delimiter used in configuration (usually a colon). Azure + // Key Vault doesn't allow a colon in secret names. + return secret.SecretIdentifier.Name + .Substring(_prefix.Length) + .Replace("--", ConfigurationPath.KeyDelimiter); + } +} From ab653291ca19305bec55d79a4946919ddaef036e Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Thu, 14 Nov 2019 15:25:41 -0600 Subject: [PATCH 2/6] Updates --- .../security/key-vault-configuration.md | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/aspnetcore/security/key-vault-configuration.md b/aspnetcore/security/key-vault-configuration.md index 7b9fbeb31a10..3757857c6e29 100644 --- a/aspnetcore/security/key-vault-configuration.md +++ b/aspnetcore/security/key-vault-configuration.md @@ -133,7 +133,7 @@ The `Certificate` sample app obtains its configuration values from `IConfigurati * `config["Section:SecretName"]` * `config.GetSection("Section")["SecretName"]` -The X.509 certificate is managed by the OS. The app calls `AddAzureKeyVault` with values supplied by the *appsettings.json* file: +The X.509 certificate is managed by the OS. The app calls with values supplied by the *appsettings.json* file: ::: moniker range=">= aspnetcore-3.0" @@ -192,8 +192,8 @@ az keyvault set-policy --name '{KEY VAULT NAME}' --object-id {OBJECT ID} --secre The sample app: * Creates an instance of the `AzureServiceTokenProvider` class without a connection string. When a connection string isn't provided, the provider attempts to obtain an access token from Managed identities for Azure resources. -* A new `KeyVaultClient` is created with the `AzureServiceTokenProvider` instance token callback. -* The `KeyVaultClient` instance is used with a default implementation of `IKeyVaultSecretManager` that loads all secret values and replaces double-dashes (`--`) with colons (`:`) in key names. +* A new is created with the `AzureServiceTokenProvider` instance token callback. +* The instance is used with a default implementation of that loads all secret values and replaces double-dashes (`--`) with colons (`:`) in key names. ::: moniker range=">= aspnetcore-3.0" @@ -211,20 +211,43 @@ When you run the app, a webpage shows the loaded secret values. In the Developme If you receive an `Access denied` error, confirm that the app is registered with Azure AD and provided access to the key vault. Confirm that you've restarted the service in Azure. +::: moniker range=">= aspnetcore-3.0" + +## Configuration options + + can accept an : + +```csharp +config.AddAzureKeyVault( + new AzureKeyVaultConfigurationOptions() + { + ... + }); +``` + +| Property | Description | +| ---------------- | ----------- | +| `Client` | to use for retrieving values. | +| `Manager` | instance used to control secret loading. | +| `ReloadInterval` | `Timespan` to wait between attempts at polling the Azure KeyVault for changes. The default value is `null` (configuration isn't reloaded). | +| `Vault` | Vault URI. | + +::: moniker-end + ## Use a key name prefix -`AddAzureKeyVault` provides an overload that accepts an implementation of `IKeyVaultSecretManager`, which allows you to control how key vault secrets are converted into configuration keys. For example, you can implement the interface to load secret values based on a prefix value you provide at app startup. This allows you, for example, to load secrets based on the version of the app. + provides an overload that accepts an implementation of , which allows you to control how key vault secrets are converted into configuration keys. For example, you can implement the interface to load secret values based on a prefix value you provide at app startup. This allows you, for example, to load secrets based on the version of the app. > [!WARNING] > Don't use prefixes on key vault secrets to place secrets for multiple apps into the same key vault or to place environmental secrets (for example, *development* versus *production* secrets) into the same vault. We recommend that different apps and development/production environments use separate key vaults to isolate app environments for the highest level of security. In the following example, a secret is established in the key vault (and using the Secret Manager tool for the Development environment) for `5000-AppSecret` (periods aren't allowed in key vault secret names). This secret represents an app secret for version 5.0.0.0 of the app. For another version of the app, 5.1.0.0, a secret is added to the key vault (and using the Secret Manager tool) for `5100-AppSecret`. Each app version loads its versioned secret value into its configuration as `AppSecret`, stripping off the version as it loads the secret. -`AddAzureKeyVault` is called with a custom `IKeyVaultSecretManager`: + is called with a custom : [!code-csharp[](key-vault-configuration/samples_snapshot/Program.cs)] -The `IKeyVaultSecretManager` implementation reacts to the version prefixes of secrets to load the proper secret into configuration: +The implementation reacts to the version prefixes of secrets to load the proper secret into configuration: [!code-csharp[](key-vault-configuration/samples_snapshot/Startup.cs)] @@ -269,7 +292,7 @@ When this approach is implemented: 1. If the app's version is changed in the project file to `5.1.0.0` and the app is run again, the secret value returned is `5.1.0.0_secret_value_dev` in the Development environment and `5.1.0.0_secret_value_prod` in Production. > [!NOTE] -> You can also provide your own `KeyVaultClient` implementation to `AddAzureKeyVault`. A custom client permits sharing a single instance of the client across the app. +> You can also provide your own implementation to . A custom client permits sharing a single instance of the client across the app. ## Bind an array to a class @@ -323,7 +346,7 @@ Configuration.Reload(); ## Disabled and expired secrets -Disabled and expired secrets throw a `KeyVaultClientException`. To prevent your app from throwing, replace your app or update the disabled/expired secret. +Disabled and expired secrets throw a . To prevent your app from throwing, replace your app or update the disabled/expired secret. ## Troubleshoot From 27bb64a4ea972428d273e7c0da3c02f6769bc4f8 Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Thu, 14 Nov 2019 15:32:03 -0600 Subject: [PATCH 3/6] Nits --- aspnetcore/security/key-vault-configuration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aspnetcore/security/key-vault-configuration.md b/aspnetcore/security/key-vault-configuration.md index e017adfd9387..6db593e14e61 100644 --- a/aspnetcore/security/key-vault-configuration.md +++ b/aspnetcore/security/key-vault-configuration.md @@ -241,8 +241,8 @@ config.AddAzureKeyVault( | ---------------- | ----------- | | `Client` | to use for retrieving values. | | `Manager` | instance used to control secret loading. | -| `ReloadInterval` | `Timespan` to wait between attempts at polling the Azure KeyVault for changes. The default value is `null` (configuration isn't reloaded). | -| `Vault` | Vault URI. | +| `ReloadInterval` | `Timespan` to wait between attempts at polling the key vault for changes. The default value is `null` (configuration isn't reloaded). | +| `Vault` | Key vault URI. | ::: moniker-end From 09922a5d960c43bcf2298a15da199ce6983c3074 Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Thu, 14 Nov 2019 16:58:59 -0600 Subject: [PATCH 4/6] Fix code alignments --- .../samples/2.x/SampleApp/Program.cs | 16 ++++++++-------- .../samples/3.x/SampleApp/Program.cs | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/aspnetcore/security/key-vault-configuration/samples/2.x/SampleApp/Program.cs b/aspnetcore/security/key-vault-configuration/samples/2.x/SampleApp/Program.cs index dd4c43252894..29cc8de83d11 100644 --- a/aspnetcore/security/key-vault-configuration/samples/2.x/SampleApp/Program.cs +++ b/aspnetcore/security/key-vault-configuration/samples/2.x/SampleApp/Program.cs @@ -69,15 +69,15 @@ public static IWebHostBuilder CreateWebHostBuilder(string[] args) => { var builtConfig = config.Build(); - var azureServiceTokenProvider = new AzureServiceTokenProvider(); - var keyVaultClient = new KeyVaultClient( - new KeyVaultClient.AuthenticationCallback( - azureServiceTokenProvider.KeyVaultTokenCallback)); + var azureServiceTokenProvider = new AzureServiceTokenProvider(); + var keyVaultClient = new KeyVaultClient( + new KeyVaultClient.AuthenticationCallback( + azureServiceTokenProvider.KeyVaultTokenCallback)); - config.AddAzureKeyVault( - $"https://{builtConfig["KeyVaultName"]}.vault.azure.net/", - keyVaultClient, - new DefaultKeyVaultSecretManager()); + config.AddAzureKeyVault( + $"https://{builtConfig["KeyVaultName"]}.vault.azure.net/", + keyVaultClient, + new DefaultKeyVaultSecretManager()); } }) .UseStartup(); diff --git a/aspnetcore/security/key-vault-configuration/samples/3.x/SampleApp/Program.cs b/aspnetcore/security/key-vault-configuration/samples/3.x/SampleApp/Program.cs index 207261133dfa..6442ccc304bc 100644 --- a/aspnetcore/security/key-vault-configuration/samples/3.x/SampleApp/Program.cs +++ b/aspnetcore/security/key-vault-configuration/samples/3.x/SampleApp/Program.cs @@ -72,15 +72,15 @@ public static IHostBuilder CreateHostBuilder(string[] args) => { var builtConfig = config.Build(); - var azureServiceTokenProvider = new AzureServiceTokenProvider(); - var keyVaultClient = new KeyVaultClient( - new KeyVaultClient.AuthenticationCallback( - azureServiceTokenProvider.KeyVaultTokenCallback)); + var azureServiceTokenProvider = new AzureServiceTokenProvider(); + var keyVaultClient = new KeyVaultClient( + new KeyVaultClient.AuthenticationCallback( + azureServiceTokenProvider.KeyVaultTokenCallback)); - config.AddAzureKeyVault( - $"https://{builtConfig["KeyVaultName"]}.vault.azure.net/", - keyVaultClient, - new DefaultKeyVaultSecretManager()); + config.AddAzureKeyVault( + $"https://{builtConfig["KeyVaultName"]}.vault.azure.net/", + keyVaultClient, + new DefaultKeyVaultSecretManager()); } }) .ConfigureWebHostDefaults(webBuilder => From 309b24854bef22eb163f4851bd740b5235e1d27b Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Thu, 14 Nov 2019 19:09:02 -0600 Subject: [PATCH 5/6] Convert samples into hideous Frankenapps! lol --- .../samples/2.x/SampleApp/Markup.cs | 47 ------------------- .../samples/2.x/SampleApp/Startup.cs | 9 ++-- .../samples/3.x/SampleApp/Markup.cs | 47 ------------------- .../samples/3.x/SampleApp/Startup.cs | 9 ++-- 4 files changed, 6 insertions(+), 106 deletions(-) delete mode 100644 aspnetcore/security/key-vault-configuration/samples/2.x/SampleApp/Markup.cs delete mode 100644 aspnetcore/security/key-vault-configuration/samples/3.x/SampleApp/Markup.cs diff --git a/aspnetcore/security/key-vault-configuration/samples/2.x/SampleApp/Markup.cs b/aspnetcore/security/key-vault-configuration/samples/2.x/SampleApp/Markup.cs deleted file mode 100644 index 0e3658941a86..000000000000 --- a/aspnetcore/security/key-vault-configuration/samples/2.x/SampleApp/Markup.cs +++ /dev/null @@ -1,47 +0,0 @@ -namespace SampleApp -{ - internal static class Markup - { - internal const string Text = @" - - - - Key Vault Configuration Provider Sample - - - -

Key Vault Configuration Provider Sample

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
SecretName in Key VaultObtained from ConfigurationValue
SecretNameSecretNameConfiguration[""SecretName""]{0}
Section:SecretNameSection--SecretNameConfiguration[""Section:SecretName""]{1}
Configuration.GetSection(""Section"")[""SecretName""]{2}
-
- - "; - } -} diff --git a/aspnetcore/security/key-vault-configuration/samples/2.x/SampleApp/Startup.cs b/aspnetcore/security/key-vault-configuration/samples/2.x/SampleApp/Startup.cs index b79b39cc2cf4..93e05c4a9fc3 100644 --- a/aspnetcore/security/key-vault-configuration/samples/2.x/SampleApp/Startup.cs +++ b/aspnetcore/security/key-vault-configuration/samples/2.x/SampleApp/Startup.cs @@ -1,7 +1,7 @@ +using System; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; -using System.Text; namespace SampleApp { @@ -18,11 +18,8 @@ public void Configure(IApplicationBuilder app) { app.Run(async context => { - var encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); - var document = string.Format(Markup.Text, Configuration["SecretName"], Configuration["Section:SecretName"], Configuration.GetSection("Section")["SecretName"]); - context.Response.ContentLength = encoding.GetByteCount(document); - context.Response.ContentType = "text/html"; - await context.Response.WriteAsync(document); + context.Response.ContentType = "text/plain"; + await context.Response.WriteAsync($@"SecretName (Name in Key Vault: 'SecretName'){Environment.NewLine}Obtained from Configuration with Configuration[""SecretName""]{Environment.NewLine}Value: {Configuration["SecretName"]}{Environment.NewLine}{Environment.NewLine}Section:SecretName (Name in Key Vault: 'Section--SecretName'){Environment.NewLine}Obtained from Configuration with Configuration[""Section:SecretName""]{Environment.NewLine}Value: {Configuration["Section:SecretName"]}{Environment.NewLine}{Environment.NewLine}Section:SecretName (Name in Key Vault: 'Section--SecretName'){Environment.NewLine}Obtained from Configuration with Configuration.GetSection(""Section"")[""SecretName""]{Environment.NewLine}Value: {Configuration.GetSection("Section")["SecretName"]}"); }); } } diff --git a/aspnetcore/security/key-vault-configuration/samples/3.x/SampleApp/Markup.cs b/aspnetcore/security/key-vault-configuration/samples/3.x/SampleApp/Markup.cs deleted file mode 100644 index 0e3658941a86..000000000000 --- a/aspnetcore/security/key-vault-configuration/samples/3.x/SampleApp/Markup.cs +++ /dev/null @@ -1,47 +0,0 @@ -namespace SampleApp -{ - internal static class Markup - { - internal const string Text = @" - - - - Key Vault Configuration Provider Sample - - - -

Key Vault Configuration Provider Sample

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
SecretName in Key VaultObtained from ConfigurationValue
SecretNameSecretNameConfiguration[""SecretName""]{0}
Section:SecretNameSection--SecretNameConfiguration[""Section:SecretName""]{1}
Configuration.GetSection(""Section"")[""SecretName""]{2}
-
- - "; - } -} diff --git a/aspnetcore/security/key-vault-configuration/samples/3.x/SampleApp/Startup.cs b/aspnetcore/security/key-vault-configuration/samples/3.x/SampleApp/Startup.cs index f511b16cefee..f90d50370d64 100644 --- a/aspnetcore/security/key-vault-configuration/samples/3.x/SampleApp/Startup.cs +++ b/aspnetcore/security/key-vault-configuration/samples/3.x/SampleApp/Startup.cs @@ -1,4 +1,4 @@ -using System.Text; +using System; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; @@ -20,11 +20,8 @@ public void Configure(IApplicationBuilder app) { app.Run(async context => { - var encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); - var document = string.Format(Markup.Text, Configuration["SecretName"], Configuration["Section:SecretName"], Configuration.GetSection("Section")["SecretName"]); - context.Response.ContentLength = encoding.GetByteCount(document); - context.Response.ContentType = "text/html"; - await context.Response.WriteAsync(document); + context.Response.ContentType = "text/plain"; + await context.Response.WriteAsync($@"SecretName (Name in Key Vault: 'SecretName'){Environment.NewLine}Obtained from Configuration with Configuration[""SecretName""]{Environment.NewLine}Value: {Configuration["SecretName"]}{Environment.NewLine}{Environment.NewLine}Section:SecretName (Name in Key Vault: 'Section--SecretName'){Environment.NewLine}Obtained from Configuration with Configuration[""Section:SecretName""]{Environment.NewLine}Value: {Configuration["Section:SecretName"]}{Environment.NewLine}{Environment.NewLine}Section:SecretName (Name in Key Vault: 'Section--SecretName'){Environment.NewLine}Obtained from Configuration with Configuration.GetSection(""Section"")[""SecretName""]{Environment.NewLine}Value: {Configuration.GetSection("Section")["SecretName"]}"); }); } } From 58369c6b24b2646b1b1760338d3add335266cbb2 Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Fri, 15 Nov 2019 09:06:53 -0600 Subject: [PATCH 6/6] Move two code comments into the text --- aspnetcore/security/key-vault-configuration.md | 5 +++++ .../key-vault-configuration/samples_snapshot/Startup.cs | 6 ------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/aspnetcore/security/key-vault-configuration.md b/aspnetcore/security/key-vault-configuration.md index 6db593e14e61..b56818c9f76f 100644 --- a/aspnetcore/security/key-vault-configuration.md +++ b/aspnetcore/security/key-vault-configuration.md @@ -261,6 +261,11 @@ In the following example, a secret is established in the key vault (and using th The implementation reacts to the version prefixes of secrets to load the proper secret into configuration: +* `Load` loads a secret when its name starts with the prefix. Other secrets aren't loaded. +* `GetKey`: + * Removes the prefix from the secret name. + * Replaces two dashes in any name with the `KeyDelimiter`, which is the delimiter used in configuration (usually a colon). Azure Key Vault doesn't allow a colon in secret names. + [!code-csharp[](key-vault-configuration/samples_snapshot/Startup.cs)] The `Load` method is called by a provider algorithm that iterates through the vault secrets to find the ones that have the version prefix. When a version prefix is found with `Load`, the algorithm uses the `GetKey` method to return the configuration name of the secret name. It strips off the version prefix from the secret's name and returns the rest of the secret name for loading into the app's configuration name-value pairs. diff --git a/aspnetcore/security/key-vault-configuration/samples_snapshot/Startup.cs b/aspnetcore/security/key-vault-configuration/samples_snapshot/Startup.cs index 8171bfd5bea7..4c221af1664d 100644 --- a/aspnetcore/security/key-vault-configuration/samples_snapshot/Startup.cs +++ b/aspnetcore/security/key-vault-configuration/samples_snapshot/Startup.cs @@ -9,17 +9,11 @@ public PrefixKeyVaultSecretManager(string prefix) public bool Load(SecretItem secret) { - // Load a vault secret when its secret name starts with the - // prefix. Other secrets won't be loaded. return secret.Identifier.Name.StartsWith(_prefix); } public string GetKey(SecretBundle secret) { - // Remove the prefix from the secret name and replace two - // dashes in any name with the KeyDelimiter, which is the - // delimiter used in configuration (usually a colon). Azure - // Key Vault doesn't allow a colon in secret names. return secret.SecretIdentifier.Name .Substring(_prefix.Length) .Replace("--", ConfigurationPath.KeyDelimiter);