How to call Key Vault from both an onprem server & an Azure App Service using .NET Framework & .NET Core

My GitHub repo shows how to retrieve secrets stored in an Azure Key Vault using a .NET Framework & .NET Core application. It demonstrates both pulling the secrets via middleware at startup time and dynamically when a page is loaded.

Onprem deployment

architecture-onprem

Onprem, you need to provide a way for the running application to access the Key Vault securly. This is accomplished via service principal provisioned in Azure Active Directory. This service principal is then granted access to the Key Vault. The application authenticates to Azure Active Directory using a X.509 certificate so that it can use the service principal to access the Key Vault.

Note: The application is written to pull the certificate from the cert:\LocalMachine\My certificate store on the onprem server. If this is not the right location to get your certificate from, you will need to modify the code to pull the certificate from the correct location.

You will need to modify code similar to the below code to pull from the right store for your deployment.

C#
var x509Store = new X509Store(StoreName.My,
                              StoreLocation.LocalMachine);

Azure deployment

architecture-azure

In Azure, the process can be simplified by using a Managed Identity. The Managed Identity is granted access to the Key Vault & is assigned to the App Service so code running in the App Service can use it. The deployment script will set the Managed Identity client ID for you as part of deployment. This will override the values specified in the configuration files.

Note: This repo intentionally doesn’t access the secrets through the Azure App Service configuration so that it is portable between onprem & Azure. If you are only targeting Azure, you can store the secrets in the App Service configuration.

Note: You could still use the certificate-based authentication method to allow your application to authenticate with Azure AD instead of using Managed Identity. Managed Identity makes the process simpler, but is not required.

Disclaimer

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

.NET Framework

The .NET Framework version of this application pulls the secrets from Key Vault when the web page is loaded. It does not have a default inversion of control container that pulls the secrets at startup time. Instead, it uses a custom implementation of the ConfigBuilder to pull the certificate from the Windows certificate store to authenticate to Azure with & pull all the secrets from Key Vault.

The following NuGet packages are required to access Key Vault using .NET Framework (these will install some additional dependencies):

  • Azure.Identity
  • Azure.Security.KeyVault.Secrets
  • Microsoft.Configuration.ConfigurationBuilders.Azure

If you look at the ./web-net-framework/Web.config file, you can see the following configBuilder section which tells teh custom CertificateAuthenticationAzureKeyVaultConfigBuilder class how to authenticate with Azure Active Directory so it can use the service principal to access the Key Vault.

XML
<configBuilders>
    <builders>
      <add name="KeyVault" mode="Greedy" vaultName="kv-keyvault-web-ussc-dev" enabled="true" certificateStoreName="My" certificateStoreLocation="LocalMachine" certificateThumbprint="a17d4362fbf40049bb4aa7eb465d082358c7878a" tenantId="72f988bf-86f1-41af-91ab-2d7cd011db47" clientId="9bfd1049-3cfe-4466-a684-2b5fb636b03e" type="web_net_framework.Services.CertificateAuthenticationAzureKeyVaultConfigBuilder, web-net-framework" />
    </builders>
  </configBuilders>
  <appSettings configBuilders="KeyVault">
    <add key="the-king-of-england" value="Elizabeth II" />
  ...
</appSettings>

You will notice that the appSettings section of the Web.config file has a value already for the-king-of-england. Naturally, this value is wrong and we want to override it with the value from Key Vault. The Greedy flag will override the value from the Web.config withe the one from Key Vault if it was able to successfully authenticate & pull secrets.

Here is what the running application should look like if it was able to successfully authenticate with Azure Active Directory & pull secrets from Key Vault.

web-net-framework

Onprem authentication with certificate

In the ./web-net-framework/Services/CertificateAuthenticationAzureKeyVaultConfigBuilder.cs, we pull the certificate from the local store, authenticate with Azure AD, then pull the secrets that are needed. This custom class is needed because the default implementation of the AzureKeyVaultConfigBuilder will try to use the DefaultAzureCredentials class which will not work on the onprem server since it doesn’t have a managed identity nor does the service account running the app pool have access to Azure. Instead, we want to pull a certificate from the local store and authenticate with Azure AD.

C#
protected override TokenCredential GetCredential()
{
    StoreName storeName = (StoreName)Enum.Parse(typeof(StoreName), CertificateStoreName);

    StoreLocation storeLocation = (StoreLocation)Enum.Parse(typeof(StoreLocation), CertificateStoreLocation);

    var x509Store = new X509Store(storeName,
                                  storeLocation);

    x509Store.Open(OpenFlags.ReadOnly);

    X509Certificate2 x509Certificate;

    try
    {
        x509Certificate = x509Store.Certificates.Find(X509FindType.FindByThumbprint,
                                                      CertificateThumbprint,
                                                      validOnly: false)
                                                .OfType<X509Certificate2>()
                                                .Single();
    }
    catch (Exception ex)
    {
        throw new ArgumentException($"Unable to find certificate in cert:\\{CertificateStoreLocation}\\{CertificateStoreName} with thumbprint: {CertificateThumbprint}", ex);
    }

    var tokenCredential = new ClientCertificateCredential(TenantId,
                                                          ClientId,
                                                          x509Certificate);

    return tokenCredential;
}

App Service with Managed Identity

Using the Managed Identity associated with the App Service, it much simpler to authenticate with Azure AD.

C#
client = new SecretClient(new Uri(kvUri),
                            new DefaultAzureCredential(new DefaultAzureCredentialOptions
                            {
                                ManagedIdentityClientId = ConfigurationManager.AppSettings["Authentication:ManagedIdentityClientId"]
                            }));

.NET Core

Similarly, the .NET Core version of this application pulls the Key Vault secrets when the web page is loaded. However, because .NET Core already has a inversion of control container installed, most of the secrets are pulled at startup time. Only 1 secret is pulled at page load time to demonstrate how to pull secrets dynamically.

If you look at the ./web-net-core/appsettings.json file, you can see the following app settings which will allow the application to authenticate with Azure Active Directory so it can use the service principal to access the Key Vault.

JSON
"KeyVaultName": "kv-keyvault-web-ussc-dev",
"Authentication": {
  "AzureADApplicationId": "9bfd1049-3cfe-4466-a684-2b5fb636b03e",
  "AzureADCertificateThumbprint": "539cd5afadb7b25b85cf90a78c261074a6db6445",
  "AzureADDirectoryId": "72f988bf-86f1-41af-91ab-2d7cd011db47",
  "ManagedIdentityClientId": ""
},
"IsHostedOnPrem": "true"

Here is what the running application should look like if it was able to successfully authenticate with Azure Active Directory & pull secrets from Key Vault.

web-net-core

Startup.cs code to pull all secrets as configuration values

The middleware allows us to pull all secrets from Key Vault at startup time and store them as configuration values that can be used throughout the application (look at the ./web-net-core/Program.cs file).

C#
builder.Configuration.AddAzureKeyVault(new Uri(kvUri), new ClientCertificateCredential(
                                          builder.Configuration["Authentication:AzureADDirectoryId"],
                                          builder.Configuration["Authentication:AzureADApplicationId"],
                                          x509Certificate));

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *