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!

Program.cs

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;
            });

            KeyVaultCache keyVaultCache = new KeyVaultCache("https://bitscryvault.vault.azure.net/secrets/", clientId, clientSecret);
            var cacheSecret = keyVaultCache.GetCachedSecret("BlogConnection");

            string connectionString = cacheSecret.Result;
        }
    }
}

KeyVaultCache.cs

using Microsoft.Azure.KeyVault;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
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 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) =>
                    {
                        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.


6 Comments

james · 24th March 2019 at 1:59 pm

Can you elaborate on how to get tenant id and subscription id? and if you can’t put your client id and client secret in this app and you don’t yet have access to key vault, what’s the solution for storing these particular secrets?

Shinigami · 24th March 2019 at 2:21 pm

The easiest way I’ve found to get subscription ID and tenant ID is from the portal. Subscription ID is visible in the “Subscriptions” blade and tenant ID is available from the “Directory + Subscription” filter on the top right.

If you can’t use key vault then you could use an appsettings file.

https://blog.bitscry.com/2017/05/30/appsettings-json-in-net-core-console-app/

Though the secrets here are obviously in the clear so it’ll depend where you’re app is running as to if this is a good idea or not.

mezmiro · 12th June 2019 at 7:53 pm

Excellent post! Thanks for sharing – this helped me out a lot, especially with the cache.

I discovered a bug where this code was trying to add kvp’s to the dictionary when they already existed though, so I had to add a check into the GetCachedSecret() function to account for this. This is likely because I’m using this in a multi-threaded context, but others might run into this bug as well.

    Shinigami · 13th June 2019 at 9:54 am

    Glad this helped you out. I hadn’t thought about using it in a multithreaded context, I’ll update the code and the post accordingly to check the keys before adding them. Thanks!

Christine · 10th July 2019 at 8:05 pm

Hi! Thank you for your post! This was helpful.
I had the same question as James. How do I retrieve the needed client secret “secretly”? It feels like I’m swapping one exposed password for another.
I’ve added the Azure Key Vault to a few of my ASP.NET Core (2.2) apps, which works like a charm. In the Core Azure Key Vault package, I have access to an Azure token provider that uses the developer’s Azure CLI to authenticate against azure. That way when running locally, my developer profile is used, and in prod, the app service’s identity is used.
Do you know of a similar mechanism when not using the WebHostBuilder?

    Shinigami · 11th July 2019 at 10:04 am

    Sorry, I’m not sure how to integrate this with managed identities as in web applications, though I don’t see why this wouldn’t be possible so I may investigate when I get some spare time.

    My use case is that these console applications will be running as an automated process on a local server so my developer profile can’t be used for authentication, I’m mainly using KeyVault here as a way of centrally managing secrets so if a connection string changes for example, I don’t have to go through and update all applications that use it.

Leave a Reply

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