9th blogpost about Azure Key Vault, this time it has become time to look at integrating Azure Key Vault into a normal Umbraco site. I'll be focusing on putting it into an Umbraco 8 site, mainly as of this writing we are at version 8.5, and version 7 does not get lot of attention anymore.
Premise - setup Umbraco site
So the premise of this blog post will be going from secrets stored in AppSettings or even static in code, and move those into Azure Key Vault, so the repository or other people that has access to the site and codebase does not have direct access to the secretes.
I've added the code used in the repository for this series found here: https://github.com/mikkelhm/Blog-KeyVaultSeries/tree/master/UmbracoWithKeyVault. It contains a Solution, and two csproj files, one for the main Umbraco site, and one for a class library that will contain the C# code used. If you are familiar with setting up an Umbraco site, you can just skip or fast-forward the next section.
The site I've setup contains the following:
- 1 Document type, named "Home". It only contains 1 property type, it's named "Body".
- 1 Template for the Home Document type, named "Home.cshtml"
- In the Class library, I got
- A "HomeController" - which inherits from "RenderMvcController" to control the model that is parsed to the Template.
- A "SecretComposer" - Which will setup a "SecretSettings" object in the dependency Injection container, for usage when needed.
- A Custom Model returned by the "HomeController" named "HomeModelWithSecrets", this model has a "Secrets" property, that will contain the "SecretSettings" object for easy access.
The SecretComposer is where it all starts, it looks like the following:
public class SecretComposer : IUserComposer { public void Compose(Composition composition) { var settingsInstance = GetSecretSettings(); composition.Register(settingsInstance); } private SecretSettings GetSecretSettings() { return new SecretSettings() { SomeSecretConnectionString = ConfigurationManager.AppSettings["SomeSecretConnectionString"], SomeSecretValue = ConfigurationManager.AppSettings["SomeSecretValue"], SomeSecretTokenToAnAmazingIntegration = ConfigurationManager.AppSettings["SomeSecretTokenToAnAmazingIntegration"] }; } }
It's hooking into the container composing in Umbraco and registers an instance of SecretSettings. This can then be used throughout the application. Right now, it's important to see that the properties in the SecretSettings object is fetched from the AppSettings element in web.config. This is what we are going to change, so it will be fetched from Azure Key Vault instead.
The custom view model I'm using called HomeModelWithSecrets. It inherits from the default Content model and extends it with a Secrets property of type SecretSettings. It looks like this:
public class HomeModelWithSecrets : ContentModel { public HomeModelWithSecrets(IPublishedContent content) : base(content) { } public SecretSettings Secrets { get; set; } }
Next up is the HomeController - This is where I'm going to fetch the SecretSettings object that was setup in the Dependency Injection Container and put it into a new instance of the HomeModelWithSecrets class, and then return it to its View. This is done with the following code:
public class HomeController : RenderMvcController { private readonly SecretSettings _secretSettings; public HomeController(SecretSettings secretSettings) { _secretSettings = secretSettings; } public override ActionResult Index(ContentModel model) { var secretModel = new HomeModelWithSecrets(model.Content) { Secrets = _secretSettings }; return CurrentTemplate(secretModel); } }
The final bits for displaying anything in our scenario is to render the values in the template. The template will need to use our new model, and from there the SecretSettings property is available.
@inherits Umbraco.Web.Mvc.UmbracoViewPage @{ Layout = null; } <html> <head> <title>@Model.Content.Name<title/> <head/> <body> <h1>@Model.Content.Value("Name")</h1> <div>@Model.Content.Value("Body")</div> <ul> <li>SomeSecretConnectionString: @Model.Secrets.SomeSecretConnectionString</li> <li>SomeSecretValue: @Model.Secrets.SomeSecretValue</li> <li>SomeSecretTokenToAnAmazingIntegration: @Model.Secrets.SomeSecretTokenToAnAmazingIntegration</li> </ul> </body> </html>
With the above in place, it is time to look at adding Key Vault into the mix.
Accessing Azure Key Vault
Ensuring that your site has access to an Azure Key Vault, does not have a lot to do with Umbraco itself. As an Umbraco site is just a ASP.NET Website, then integrating Azure Key Vault, will be done the same way. Take a look at part 4 of this series, where I wrote about "Client Id/ApplicationId vs Certificate based access". In this blob post ill run through the Client Id/ApplicationId method. The reason for this is that it is the most approachable method of adding Azure Key Vault to your Umbraco site. Ill also start from scratch, but the first parts will be run through fast, and done via the Azure Cli. I've written a little more details about in the 2nd blog post, both Cli and via the Azure Portal.
Creating a new Azure Key Vault and getting access to it
If you already have a Key Vault and a Service principal+Client secret, then just skip this part, else - there is 4 parts involved in creating a new instance.
- Create a new Resource Group for your solution (optional, you can reuse an existing resource group)
- Create a new Azure Key Vault
- Create a Service Principal (Managed Identity) in Azure that has access to the Key Vault
- Generate a Client secret that can be used to authenticate the Service Principal to the Key Vault
We'll do this via the Azure Cli - I'm using the Cli as it's by far the fastest way of doing it, but you must have the Cli installed, and be comfortable with it. If you don't have it installed or prefer a UI instead, check the guides in the part 2 blog post.
az group create -n UmbracoWithKeyVault -l westeurope az keyvault create -n MikkelhmUmbracoKeyVault -g UmbracoWithKeyVault -l westeurope az ad sp create-for-rbac -n MikkelhmUmbracoKeyVaultSP --skip-assignment --years 5
That last command returns the appId and the password for the service principal, we'll store that one, as it is our Client secret, I got appId: "c7ed6bd5-da9c-43ff-a2bf-e1ce14bd7270" and password: "a252ba11-8d4c-4e3a-bac9-bbbaf6764651". We will use these to access the Key Vault. Last command is allowing the Service principal to access the Key Vault
az keyvault set-policy -n MikkelhmUmbracoKeyVault --spn c7ed6bd5-da9c-43ff-a2bf-e1ce14bd7270 --secret-permission get
Setting up Azure Key vault access from Umbraco
Now it all comes back to accessing the secrets in Key vault. I've previously written about how this can be done in Part 3 of the series, but let me refresh it here.
We need to install two NuGet packages that will handle the integration
- Microsoft.Azure.KeyVault - currently version 3.0.5 - is the main library for accessing Key Vault
- Microsoft.IdentityModel.Clients.ActiveDirectory - currently version 5.2.6 - used to authorize with Key Vault
With the two packages added, it's time to update the Composer to look like this:
public class SecretComposer : IUserComposer { private readonly KeyVaultClient _keyVaultClient; public SecretComposer() { _keyVaultClient = new KeyVaultClient(GetToken); } public void Compose(Composition composition) { var settingsInstance = GetSecretSettings(); composition.Register(settingsInstance); } private SecretSettings GetSecretSettings() { return new SecretSettings() { SomeSecretConnectionString = GetKeyVaultSecret("SomeSecretConnectionString"), SomeSecretValue = GetKeyVaultSecret("SomeSecretValue"), SomeSecretTokenToAnAmazingIntegration = GetKeyVaultSecret("SomeSecretTokenToAnAmazingIntegration") }; } private string GetKeyVaultSecret(string secretKey) { var secret = _keyVaultClient.GetSecretAsync(ConfigurationManager.AppSettings["KeyVaultRootUrl"], secretKey).GetAwaiter().GetResult(); return secret?.Value; } public static async Task GetToken(string authority, string resource, string scope) { var authContext = new AuthenticationContext(authority); ClientCredential clientCred = new ClientCredential(ConfigurationManager.AppSettings["ClientId"], ConfigurationManager.AppSettings["ClientSecret"]); AuthenticationResult result = await authContext.AcquireTokenAsync(resource, clientCred); if (result == null) throw new InvalidOperationException("Failed to obtain the JWT token"); return result.AccessToken; } }
The important part in the above is the GetToken method which handles getting a token from Azure that will be used to access the Key vault secrets. I've added the ClientId, ClientSecret and KeyVaultRootUrl to the web.config in the AppSettings element.
<add key="KeyVaultRootUrl" value="https://mikkelhmumbracokeyvault.vault.azure.net/" /> <add key="ClientId" value="c7ed6bd5-da9c-43ff-a2bf-e1ce14bd7270" /> <add key="ClientSecret" value="a252ba11-8d4c-4e3a-bac9-bbbaf6764651" />
I created the secrets in the Key vault, so they would render on the page. This is of course not how secrets are supposed to be used, but it proves that they are fetched from Key vault, rather than from config. The page now looks like this:
The integration should prove that we now have a SecretSettings object that is registered in the Dependency Injection container. The object can be used throughout our application, when we need to pass secrets to other services, like 3rd party integrations.
Should those ClientId and Client Secret not be hidden as well?
The ClientId and the Client Secret is now the only entry into Key vault and the secrets we have stored there. This means that in order to get secrets, you need to use the ClientId and the Client Secret, and use the integration used here.
This also means that anyone that has those two strings has access to all the secrets stored in the Key vault. This can be an issue, as we wouldn't necessarily want everyone that has access to the source code to have access to the secrets.
There are a few ways of handling the issue
- Different Key vaults pr. environment you have, OR add fallback, so when developing locally you fetch secrets from the web.config, when not local it will access Key vault.
- Don't set app settings in web.config for other environments than local/dev - set them explicit on the web app that will be hosting your site.
- Certificate based access or use Azure managed identity
That's it for integrating Azure Key Vault into an Umbraco site. It can be taken much further, but with these bits, you should be up and running and have the initial integration running.