Setting up Azure Key Vault in Kentico Xperience 13

23/10/2023

Azure Key Vault is a cloud-based service designed to secure sensitive information, keys, and secrets that should not be hardcoded into your applications.

It proves particularly valuable when you need to store and manage items like database connection strings, API keys, or any other confidential information you wish to protect. Let’s take a look at how to configure Kentico Xperience 13 to work with Azure Key Vault.

Presentation website – ASP.NET Core

Considering the substantial emphasis on security in modern times, it's unsurprising to discover a huge number of online resources related to Azure Key Vault and ASP.NET Core. There are plenty of online tutorials and code samples across several platforms and programming languages.

When implementing Azure Key Vault for ASP.NET Core websites, I typically follow this example provided by Microsoft. This guide explains how to configure an ASP.NET Core application to use managed identities to authenticate and access secrets from Azure Key Vault. This is perfect for setting up Azure Key Vault with Kentico Xperience 13 presentation websites. The implementation focuses around installing the following NuGet packages and using the extension method to register the Key Vault instance.

  • Azure.Extensions.AspNetCore.Configuration.Secrets
  • Azure.Identity
  • Azure.Security.KeyVault.Secrets

An example implementation I have used recently (not using the new minimal hosting model) is below:

                .ConfigureAppConfiguration((context, config) =>
                {
                    if (context.HostingEnvironment.IsProduction() || context.HostingEnvironment.IsUAT())
                    {
                        var builtConfig = config.Build();

                        var keyVaultUrl = $"https://{builtConfig["KeyVaultName"]}.vault.azure.net/";

                        var secretClient = new SecretClient(
                            new Uri(keyVaultUrl),
                            new DefaultAzureCredential());

                        config.AddAzureKeyVault(secretClient, new KeyVaultSecretManager());
                    }
                })

Administration website - ASP.NET Web Forms

A slightly more challenging task when setting up Azure Key Vault for Kentico Xperience 13 is configuring the administration portal. The challenge stems from the admin portal being built on older ASP.NET Web Forms technology, making it less straightforward to find relevant resources to set up the configuration.

I found that the most straightforward approach of connecting the admin site with Azure Key Vault was through creating a custom implementation of the IConnectionStringService and IAppSettingsService interfaces.

In my custom implementations I offer a fall-back option to Kentico, by checking for the appropriate connection string or app setting in Azure Key Vault. This only happens if a value isn't found within the usual locations, I would then perform a lookup in Azure Key Vault and try to retrieve the necessary secret.

Let's take a look at some of the code! πŸ‘€

Both of these implementations are based on the default Kentico services, and both use the following methods for accessing Key Vault secrets:

        private static async Task<SecretBundle> GetSecretFromKeyVaultAsync(KeyVaultClient keyVaultClient, string secretName)
        {
            var keyVaultName = ConfigurationManager.AppSettings["KeyVaultName"];
            var keyVaultUrl = $"https://{keyVaultName}.vault.azure.net/";

            try
            {
                var secretIdentifier = $"{keyVaultUrl}secrets/{secretName}";
                return await keyVaultClient.GetSecretAsync(secretIdentifier);
            }
            catch (KeyVaultErrorException ex)
            {
                // Handle any exception occurred during the secret retrieval process.
                Console.WriteLine($"Error retrieving secret: {ex.Message}");
                return null;
            }
        }

        private static async Task<string> GetAccessTokenAsync(string authority, string resource, string scope)
        {
            var azureServiceTokenProvider = new AzureServiceTokenProvider();
            return await azureServiceTokenProvider.GetAccessTokenAsync(resource);
        }

GetSecretFromKeyVaultAsync provides the custom services a simple method for retrieving secrets from the key vault and returning the SecretBundle to the caller, which contains the secret value inside.

GetAccessTokenAsync is another simple method that generates an access token for the given resource, this is important as it is required to authenticate the admin site with the Azure Key Vault instance.

Next we'll look at how I'm using these methods to look up a connection string in the custom IConnectionStringService implementation.

        /// <summary>
        /// Gets the specific connection string from the app config																
        /// </summary>
        public string this[string name]
        {
            get
            {
                // Get connection string
                var connString = SettingsHelper.ConnectionStrings[name];
                if (connString == null)
                {
                    if (!name.Equals("CMSConnectionString", StringComparison.InvariantCultureIgnoreCase))
                    {
                        return null;
                    }

                    return GetAzureKeyVaultConnectionString();
                }

                return connString.ConnectionString;
													   
            }
            set
            {
                if (String.IsNullOrEmpty(name))
                {
                    throw new ArgumentException(nameof(name));
                }

                SettingsHelper.ConnectionStrings[name] = new ConnectionStringSettings(name, value);
            }
        }

        private string GetAzureKeyVaultConnectionString()
        {
            var keyVaultClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(GetAccessTokenAsync));

            var connectionString = Task.Run(() => GetSecretFromKeyVaultAsync(keyVaultClient, "ConnectionStrings--CMSConnectionString")).GetAwaiter().GetResult();

            var connectionStringValue = connectionString?.Value;

            if (!string.IsNullOrWhiteSpace(connectionStringValue))
            {
                return connectionStringValue;
            }

            return null;
        }

I start by amending Kentico's default indexer property, if there isn't a CMSConnectionString already added, then we call the new method GetAzureKeyVaultConnectionString. This method calls the previous methods to authenticate and retrieve the ConnectionStrings--CMSConnectionString secret from Azure Key Vault which contains our database connection string.

Now we'll look at how I customised the AppSettingsService.

        // Added a whitelist of app settings so we're only looking up keys we want to.
        private static readonly HashSet<string> WhiteListedAppSettings = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
        {
            "CMSAzureAccountName",
            "CMSAzureSharedKey",
        };

        /// <summary>
        /// Gets the specific settings from the application configuration file or from a memory.
        /// Sets a specific settings with a value into a memory.
        /// </summary>
        public string this[string key]
        {
            get
            {
                var appSetting = SettingsHelper.AppSettings[key];
                if (appSetting == null)
                {
                    if (!WhiteListedAppSettings.Contains(key))
                    {
                        return null;
                    }

                    return GetAzureKeyVaultAppSetting(key);
                }


                return SettingsHelper.AppSettings[key];
            }
            set
            {
                SettingsHelper.AppSettings[key] = value;
            }
        }

        private string GetAzureKeyVaultAppSetting(string key)
        {
            var keyVaultClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(GetAccessTokenAsync));

            var appSetting = Task.Run(() => GetSecretFromKeyVaultAsync(keyVaultClient, key)).GetAwaiter().GetResult();

            var appSettingValue = appSetting?.Value;

            if (!string.IsNullOrWhiteSpace(appSettingValue))
            {
                return appSettingValue;
            }

            return null;
        }

Similar to the connection string example, I am changing Kentico's default indexer property, but this time I'm restricting the keys that we want to look up - to those that are stored within the white list. GetAzureKeyVaultAppSetting only really differs to GetAzureKeyVaultConnectionString in that it accepts a key as a parameter and then uses that to do the look up in Azure Key Vault.

You can find the full example code for both custom service implementations in this gist.

The future – Xperience by Kentico

The challenges mentioned here for Kentico Xperience 13 are a thing of the past with Xperience by Kentico. Xperience by Kentico is developed using the latest ASP.NET Core versions and seamlessly integrates the administration portal with the presentation website (added through a NuGet package). Now we can follow the many tutorials and code samples that are available for ASP.NET Core for both our administration portal and the presentation website!