Quote of the Day

more Quotes

Categories

Get notified of new posts

Buy me coffee

  • Home>
  • .NET>

How to retrieve connection strings in azure key vault from ASP.NET using configuration builders, XML transformation and azure devops.

Published May 19, 2020 in .NET , Azure , Devops - 7 Comments

Configuration builders are mechanisms to retrieve connection strings from external sources. Using configuration builders, you may not have to do much codings besides installing packages and providing XML configurations for connecting to popular sources. In this post, I share with you my experience in using configuration builders for .NET to securely retrieve connection strings from an azure key vault. I’ll go over the setup and share some of the issues I face while integrating my app with azure key vault.

What are configuration builders for .NET

A configuration builder is a class which exposes metadata and methods for retrieving values for keys specified in <appSettings> or connectionStrings section of a web.config or appsettings file. Microsoft provides configuration builders for connecting to popular sources such as secrets.json, azure key vault, environment variables etc … You can also implement your own configuration builder to connect to any source you desire.

Configuration builders provide a modern and agile mechanism for ASP.NET apps to get configuration values from external sources.

Configuration builders for ASP.NET

For my ASP.NET app, I use two configuration builders which Microsoft provides: Microsoft.Configuration.ConfigurationBuilders.UserSecretsConfigBuilder and Microsoft.Configuration.ConfigurationBuilders.AzureKeyVaultConfigBuilder

From the document, configuration builders:

. Are available in .NET Framework 4.7.1 and later.

. Provide a flexible mechanism for reading configuration values.

. Address some of the basic needs of apps as they move into a container and cloud focused environment.

. Can be used to improve protection of configuration data by drawing from sources previously unavailable (for example, Azure Key Vault and environment variables) in the .NET configuration system.

Configuration builders for ASP.NET

Overall strategy

Below I outline the high level plan I use to remove connection strings from source codes:

  1. Update app to target .NET framework 4.7.1 or later.
  2. For local environment, utilize UserSecretsConfigBuilder and .NET secret management to store and retrieve connection strings in secrets.json, which is a file that resides on your computer and links with your project via secret manager.
  3. For other environments (Development, Staging, Production etc ..), uses AzureKeyVaultConfigBuilder to store and retrieve connection strings in an azure key vault.
  4. Have a web.config file for each other environment in which you specify XML transformation rules to apply configurations for connecting to specific key vaults.
  5. In an azure release pipeline, use File Transforms feature to transform web.config with configurations in your web.{environment}.config file.

In the following sections, I’ll go over the details and share codes.

Update your app to target .NET Framework 4.7.1 or later

Per the document, if your app targets a version of .NET older than 4.7.1, you need to upgrade to 4.7.1 or later. For my project, I target .NET 4.8 as it is the latest version at the time of writing.

Store and retrieve connection strings using UserSecretsConfigBuilder

Secret management is a tool to link a XML file outside of your source codes. By default, the file’s name is secrets.xml. At runtime, the framework matches the names in secrets.xml with connection strings in web.config or appsettings.config and replaces connection strings with values in secrets.xml file.

User Secret store is a file saved under user profiler folder, so secrets are not checked in to source control.

Save secret settings in User Secret store that is outside of source control folder

The easiest way to use secret management is using Visual Studio. You just need to right click on your project, select Manage User Secrets

Manage user secrets with secret manager.

What the tool does is installing necessary nuget packages, generating an id for your secret file, create a file under the folder with the id as the name, and insert relevant configurations in web.xml file. For example, I use Windows 10, and for me, the tool creates the file under: C:UserstboAppDataRoamingMicrosoftUserSecretsffc6ae13-97c8-489c-b519-3f724580a587.

In my web.xml, the tool inserts the following:

  <configSections>
  <section name="configBuilders" type="System.Configuration.ConfigurationBuildersSection, System.Configuration, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" restartOnExternalChanges="false" requirePermission="false" />
  </configSections>
  <configBuilders>
    <builders>
    <add name="Secrets" userSecretsId="ffc6ae13-97c8-489c-b519-3f724580a587" type="Microsoft.Configuration.ConfigurationBuilders.UserSecretsConfigBuilder, Microsoft.Configuration.ConfigurationBuilders.UserSecrets, Version=1.0.0.0, Culture=neutral" />
    </builders>
 </configBuilders>

The tool also installs the packages below to my app:

<package id="Microsoft.Configuration.ConfigurationBuilders.Base" version="2.0.0" targetFramework="net48" />
  <package id="Microsoft.Configuration.ConfigurationBuilders.UserSecrets" version="2.0.0" targetFramework="net48" />

My secrets.xml looks like the following:

<?xml version="1.0" encoding="utf-8"?>
<root>
  <secrets ver="1.0">
    <secret name="MyConnectionString" value="insert your connection string here"></secret>
  </secrets>
</root>

In my web.config,

 <connectionStrings configBuilders="Secrets">
    <add name="MyConnectionString" providerName="System.Data.SqlClient" connectionString="from secrets" />
  </connectionStrings>

Notice the value of configBuilders is Secrets which matches with the name under configSections -> builders. You can use a different name, it just has to match with what you specify in the builders section.

Store and retrieve connection strings using AzureKeyVaultConfigBuilder

It is fairly straightforward to add a configuration builder for integrating with an azure key vault since the setup is similar to that for secrets.xml file.

First, ensure your project has the following nuget packages:

<package id="Microsoft.Configuration.ConfigurationBuilders.Azure" version="1.0.1" targetFramework="net48" />
  <package id="Microsoft.Configuration.ConfigurationBuilders.Base" version="1.0.1" targetFramework="net48" />
 <package id="Azure.Core" version="1.0.0" targetFramework="net48" />
 <package id="Azure.Identity" version="1.0.0" targetFramework="net48" />
  <package id="Azure.Security.KeyVault.Secrets" version="4.0.0" targetFramework="net48" />
<package id="Microsoft.Azure.KeyVault" version="3.0.5" targetFramework="net48" />
  <package id="Microsoft.Azure.KeyVault.WebKey" version="3.0.5" targetFramework="net48" />
  <package id="Microsoft.Azure.Services.AppAuthentication" version="1.4.0" targetFramework="net48" />

In the above snippets, I show the nuget versions that work for my project. If your project targets a different .NET framework version, you should check the documents or you may have to go through a couple trials to get the versions that work for your project. I have run into issues because of incompatible nuget packages.

Caution:
You may run into issues if the versions of the libraries are not compatible with one another. For example, Microsoft.ConfigurationBuilders.Azure version 1.0.1 may not work with Microsoft.Configuration.ConfigurationBuilders.Base version 2.0.

Once you have installed the package, add the configuration builder in web.config:

<configBuilders>
    <builders>
    <add name="AzureKeyVault" 
uri="[VaultUri]"
type="Microsoft.Configuration.ConfigurationBuilders.AzureKeyVaultConfigBuilder, Microsoft.Configuration.ConfigurationBuilders.Azure, Version=1.0.0.0, Culture=neutral" />
    </builders>
 </configBuilders>

In the above snippets, notice I have uri=[VaultUri]. Take the note of this as I’m going to show how I use azure devops to do XML transformation in a release pipeline to replace [VaultUri] with the URI for connecting to my key vault.

In your <connectionStrings> section, set the configBuilders attribute to use the azure key vault config builder.

<connectionStrings configBuilders="AzureKeyVault">
<add name="MyConnectionString" providerName="System.Data.SqlClient" connectionString="from key vault" />
  </connectionStrings>

In your key vault, the name of your secret should match with the name of your connection string. Following the above example, you would have name your secret “MyConnectionString” in your key vault.

You may wonder how does the app authenticate to access key vault? The magic is in the Microsoft.Azure.Services.AppAuthentication package. In the absence of a connection string, the library automatically tries other methods to authenticate against azure: Managed Service Identity, Azure CLI, and Visual Studio. My app runs on an azure VM with managed identity enabled, and so I do not have to specify a connection string. I believe the current version of AzureKeyVaultConfigBuilder disallows specifying a connection string since it defeats the purpose of not having a connection string in the source code. If your app does not run on an azure resource with managed identity, you may need to look into configuring authentication via the other methods.

If everything works out right, you should be able to retrieve connection strings from your key vault.

In real projects, we usually have to deal with multiple environments, and pull connection strings from different key vaults. In addition, for local development, we may want to just use secret management. One way of handling multiple environments is using azure devops.

Apply different configurations for multiple environments

In a typical ASP.NET project, you usually have these Web.config files Web.config, Web.Debug.config and Web.Release.config. When building the project with MSBuild, you can specify a configuration flag. For instance, if you build with Release configuration, MSBuild applies XML transformation logic you specify in Web.Release.config file to transform Web.xml. At the end of the build, you have an artifact with Web.config.xml of which some configurations have been updated per your XML transformation logic.

For instance, the command below would apply XML transformation logic in Web.Release.config.xml to modify the content in Web.config.xml.

"C:Program Files (x86)Microsoft Visual Studio2019EnterpriseMSBuildCurrentBinmsbuild.exe" "D:a1sASMAngularASMangular.sln" /nologo /nr:false /dl:CentralLogger,"D:a_tasksVSBuild_71a9a2d3-a98a-4caa-96ab-affca411ecda1.166.0ps_modulesMSBuildHelpersMicrosoft.TeamFoundation.DistributedTask.MSBuild.Logger.dll";"RootDetailId=3985d9c0-f0b7-4859-974e-6de12c486b5e|SolutionDir=D:a1sASMAngular"*ForwardingLogger,"D:a_tasksVSBuild_71a9a2d3-a98a-4caa-96ab-affca411ecda1.166.0ps_modulesMSBuildHelpersMicrosoft.TeamFoundation.DistributedTask.MSBuild.Logger.dll" /p:DeployOnBuild=true /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:PackageLocation="D:a1a" /p:platform="Any CPU" /p:configuration="Release" /p:VisualStudioVersion="16.0" /p:_MSDeployUserAgent="VSTS_dbbe18ec-e0b5-4828-8485-4939250dac49_build_98_0"

As an example, I have the following transformation logic in my Web.Development.config.xml to retrieve connection strings in our key vault for development environment.

<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">

  <configBuilders>
    <builders>
      <add name="AzureKeyVault" xdt:Transform="SetAttributes" xdt:Locator="Match(name)"
           uri="https://myvaultdev.vault.azure.net/"/>
    </builders>
  </configBuilders>
  <connectionStrings configBuilders="AzureKeyVault"
                   xdt:Transform="SetAttributes">
  </connectionStrings>

</configuration>

The above snippets search for the <builders> section with name AzureKeyVault and sets the uri attribute with value to connect to the key vault. It also updates <connectionStrings> section in Web.config.xml to use the config builder AzureKeyVault.

As you can see, we can have a Web.config file with contain the core configurations, and multiple Web.{environment}.config files, each with specific configurations for the different environments. Next, I’ll show you how to use azure devops to transform Web.config at deployment time to achieve “build once, deploy everywhere”.

Using File Transform in an azure release pipeline to transform Web.config.

Azure devops has the template IIS deployment which has everything you need to deploy your app to IIS. Under IIS Web App Deploy task, you can enable XML transformation to run transformation rules on the machine to where you want to deploy the app. Azure devops pick the right file to fetch transformation logic based on value of Stage.

For example, in my release pipeline, I have Development stage which I have tasks for deploying the app to our development server. When the release pipeline runs, azure devops agent automatically apply transformation logic in my Web.Development.config xml file to Web.config xml file.

Enable XML transformation when deploying app using an azure devops release pipeline

Below snippets show the relevant sections in the resulting Web.config file after applying transformations:

<configuration>
  <configSections>
 <section name="configBuilders" type="System.Configuration.ConfigurationBuildersSection, System.Configuration, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" restartOnExternalChanges="false" requirePermission="false" />
  </configSections>
  <configBuilders>
    <builders>
    <add name="Secrets" userSecretsId="ffc6ae13-97c8-489c-b519-3f724580a587" type="Microsoft.Configuration.ConfigurationBuilders.UserSecretsConfigBuilder, Microsoft.Configuration.ConfigurationBuilders.UserSecrets, Version=1.0.0.0, Culture=neutral" />
    <add name="AzureKeyVault" uri="https://myvaultdev.vault.azure.net/" type="Microsoft.Configuration.ConfigurationBuilders.AzureKeyVaultConfigBuilder, Microsoft.Configuration.ConfigurationBuilders.Azure, Version=1.0.0.0, Culture=neutral" />
    </builders>
  </configBuilders>
  <connectionStrings configBuilders="AzureKeyVault">
    <add name="MyConnectionString" providerName="System.Data.SqlClient" connectionString="from key vault" />
  </connectionStrings>
</configuration>

If everything works out, you can declare your codes free of sensitive info. However, if this is the first time you use configuration builders, you may run into issues. In the next section, I share some of the errors I have encountered while working with the library.

Errors you may encounter

If you an encounter an error, one tip to help with debugging is to run the app in a local browser as the framework may not show as much details if you access the app remotely. Another tip is to set <customErrors mode="Off"> in your web.xml for development/test environment, as that would allow you to see error details.

When <customErrors> is enabled, ASP.NET does not show detailed error information. 

HOW TO: Troubleshoot ASP.NET Web Applications

A third tip I have learned is to create a simple console application with minimal codes to test the configs.

Below are some of the errors I have encountered while integrating my app with azure key vault using config builders.

Could not load file or assembly Microsoft.Configuration.ConfigurationBuilders.Azure

The error above may have to do with incompatible nuget packages. Review your list of dependencies and make sure they are compatible with one another.

Another issue with loading assembly

The above error is really misleading because it seems to indicate the library is trying to pull a secret named “LocalSqlServer” from the key vault. I spent quite sometimes looking at the source code of AzureKeyVaultConfigBuilder to see why it would try to get that secret. However, it turns out this error is because of not able to load Azure.Core package, as indicated in the Assembly load Trace section.

Issue with httpRuntime

You may get the above error if you use an older version of HttpRunTime that targets .NET Framework 4.7.1 or below. In your Web.config file, makes sure httpRunTime targets .NET Framework 4.7.1 or above.

  <httpRuntime targetFramework="4.7.1" />

One last error I want to share is the exception:

Tried the following 4 methods to get an access token, but none of them worked.

I got this error because of using an old version of Microsoft.Azure.Services.AppAuthentication. After I updated the version of the library to 1.4.0, the issue goes away.

That’s it. Hopefully, you now have an idea how to use configuration builders, one Web.config file for each environment, XML transformation and azure devops to pull configurations for different environments without having to do much boilerplate codes.

References

Azure key vault config builder

Securely save secret application settings for a web application

Web config transformations – the definitive syntax guide

Web.config Transformation Syntax for Web Application Project Deployment

Azure devops file transform and variable substitution reference

How to use Web.Config customErrors for ASP.NET

7 comments