Azure Key Vault is a pretty handy way of centrally managing access to secrets and logging what process has requested access to them. The best way to use it is for Azure hosted resources such as Web Applications or VMs for which you can assign a managed identity to the resource and grant this identity access to the vault. However, if you want to access vault secrets from a console application running on a local server you’ll need to do it via a service principle.

A service principle can be created in the Azure Active Directory blade in the portal, this is done by registering an app (it doesn’t need an endpoint) and noting down the Application (client) ID and Directory (tenant) ID as well as the name you gave it. This service principle then needs to be granted rights to access the key vault, this can be done in the Access policies blade in the Key Vault where rights can be granted separately for keys, secrets and certificates. Annoyingly it’s not possible to assign access to only specific keys in the vault to a service principle with the service principle having access to everything there provided you’ve granted it the relevant permissions.

The below code is an example of how to access Key Vault keys in a console application that will run from a local server, I have granted my service principle AzureResourceReport get permissions on both keys and secrets. In a real application the client ID and secret obviously shouldn’t be hard coded in the code!

using AzureResourceReport.Models;
using Microsoft.Azure.KeyVault;
using Microsoft.Azure.Management.ResourceManager.Fluent;
using Microsoft.Azure.Management.ResourceManager.Fluent.Authentication;
using Microsoft.IdentityModel.Clients.ActiveDirectory;

namespace AzureResourceReport
{
    class Program
    {
        static void Main(string[] args)
        {
            // Authenticate
            string clientId = "MYCLIENTID";
            string clientSecret = "MYCLIENTSECRET";

            AzureCredentials credentials = SdkContext.AzureCredentialsFactory.FromServicePrincipal(clientId, clientSecret, tenantId, AzureEnvironment.AzureGlobalCloud).WithDefaultSubscription(subscriptionId);

            var keyClient = new KeyVaultClient(async (authority, resource, scope) =>
            {
                var adCredential = new ClientCredential(clientId, clientSecret);
                var authenticationContext = new AuthenticationContext(authority, null);
                return (await authenticationContext.AcquireTokenAsync(resource, adCredential)).AccessToken;
            });

			// Connect to Key Vault using Client ID and Secret
            KeyVaultCache keyVaultCache = new KeyVaultCache("https://bitscryvault.vault.azure.net/secrets/", clientId, clientSecret);
			// Or use Managed Identity
            //AzureServiceTokenProvider azureServiceTokenProvider = new AzureServiceTokenProvider();
            //KeyVaultClient keyVaultClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(azureServiceTokenProvider.KeyVaultTokenCallback));
			
            var cacheSecret = keyVaultCache.GetCachedSecret("BlogConnection");

            string connectionString = cacheSecret.Result;
        }
    }
}

EDIT: The below code has been updated to reflect the breaking changes from MSAL.NET 2 to MSAL.NET 3.

EDIT 2: I’ve updated the constructor to accept a KeyVaultClient, this means a client can be authenticated with Managed Identity if desired.

using Microsoft.Azure.KeyVault;
using Microsoft.Identity.Client;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace AzureResourceReport.Models
{
    public class KeyVaultCache
    {
        public KeyVaultCache(string baseUri, string clientId, string clientSecret)
        {
            BaseUri = baseUri;
            ClientId = clientId;
            ClientSecret = clientSecret;
        }

        public KeyVaultCache(string baseUri, KeyVaultClient keyVaultClient)
        {
            BaseUri = baseUri;
            _KeyVaultClient = keyVaultClient;
        }

        public static string BaseUri { get; set; }

        public static string ClientId { get; set; }

        public static string ClientSecret { get; set; }

        private static KeyVaultClient _KeyVaultClient = null;

        private static Dictionary<string, string> SecretsCache = new Dictionary<string, string>();

        public async Task<string> GetCachedSecret(string secretName)
        {
            if (!SecretsCache.ContainsKey(secretName))
            {
                if (_KeyVaultClient is null)
                {
                    _KeyVaultClient = new KeyVaultClient(async (authority, resource, scope) =>
                    {
                        IConfidentialClientApplication confidentialClientApplication = ConfidentialClientApplicationBuilder
                            .Create(ClientId)
                            .WithClientSecret(ClientSecret)
                            .WithAuthority(authority)
                            .Build();

                        AuthenticationResult authenticationResult = await confidentialClientApplication
                            .AcquireTokenForClient(new string[] { "https://vault.azure.net/.default" })
                            .ExecuteAsync();

                        return authenticationResult.AccessToken;

                        //var adCredential = new ClientCredential(ClientId, ClientSecret);
                        //var authenticationContext = new AuthenticationContext(authority, null);
                        //return (await authenticationContext.AcquireTokenAsync(resource, adCredential)).AccessToken;
                    });
                }

                var secretBundle = await _KeyVaultClient.GetSecretAsync($"{BaseUri}{secretName}").ConfigureAwait(false);
                SecretsCache.Add(secretName, secretBundle.Value);
            }

            return SecretsCache.ContainsKey(secretName) ? SecretsCache[secretName] : string.Empty;
        }
    }
}

The KeyVaultCache is used to cache previously retrieved secrets to improve performance as there is some overhead with fetching them from the Key Vault. The above code is based on that from this post.


0 Comments

Leave a Reply

Avatar placeholder

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