Using Managed Service Identities in Functions to Access Key Vault

In my previous blog post I walked through a scenario leveraging Azure Functions and Event Grid to handle blob replication between storage accounts. There was one piece of that solution that I wasn't particular fond of, and that was the fact that I had the storage account connection string hard coded in the Function itself. This worked fine, but exposes a design flaw from a security standpoint, as you want to avoid putting credentials into code or configuration files if at all possible. We could store these credentials as environment variables in the function app, but we this doesn't provide full separation between the application and the credentials themselves. In this post, we're going to enhance this solution and use the relatively recently announced Managed Service Identity to access the connection string that we'll store in Azure Key Vault, eliminating the need for us to put any sensitive information into code, application settings or configuration files.

Managed Service Identity?

Let's take a minute and talk about what Managed Service Identity is and why it's awesome. Managed Service Identity (MSI) allows you to assign an Azure AD identity to an instance of a supported Azure service. With this identity, you can then take full advantage of RBAC to grant access to resources, and Azure AD handles the full lifecycle of the identity, including credential rolling and cleanup upon deletion. MSI can presently be used in Windows and Linux Azure VMs, App Service, Functions and Data Factory v2 to access any Azure resource that supports AAD authentication. As an example, I can assign a MSI to a Linux VM, grant access to Azure Service Bus to the MSI for the VM, and code running on the VM can use this identity to access Service Bus without having to embed connection strings or credentials in any code or configuration files. This dramatically improves the security of your application and protects your credentials from being exposed. A big win from a security perspective!

One last thing to note is that as of the writing of this blog managed service identities are currently in public preview, so all the usual disclaimers around preview services apply here. I'd expect to see this GA in the not too distant future, and would also anticipate that you'll see many more services introduce MSI support.

Enable Managed Service Identity

Enabling managed service identity on a function is pretty easy. In the Azure portal, navigate to your function app, select the Platform features tab, and click on Managed service identity.

Change the Register with Azure Active Directory option to on, click save, and that's it! We now have an AAD identity that our function can use.

 

Key vault configuration

Now let's get a Key Vault up and running, and configure it so that we can store our connection string. In the Azure portal click + Create a resource, search for Key Vault and click Create. Enter a name for your key vault, put it in a resource group, select the region and select your pricing tier (Standard is appropriate for this demo). Go ahead and click on Access policies, and Add new to add a new access policy. Click on Select principal and search for the name of your function app. In my case I named mine jbstoragereplication, and you can see that it shows up as a principal that I can select. Select your principal and then click Select. Then, in the Secret permissions drop down, check the boxes next to Get and List, which will allow this identity to get the credentials it needs out of this key vault. Click Ok on the panels and then finally click Create.

Now let's add the secrets to the key vault. Go to your primary storage account, then to the Access keys panel and you'll find your connection string. Go ahead and copy the connection string for key one, then hop back over to your key vault.

In the key vault click on the Secrets panel and click + Add to add a new secret. In the Upload options drop down select Manual, give your secret a name (I used sourceStorageConnectionString to match the variable name in our function) and then paste your connection string into the Value box. Click Create to create the new secret. Now do the same steps again to add the connection string for the second storage account. I named this secret destinationStorageConnectionString to match the variable in the function again.

So at this point we have an identity for our function and a key vault containing our connection strings for the storage accounts. Let's finish this up by updating our function to use these services.

Update function code to access key vault

We need to add a couple dependencies to our function so we can use key vault and MSI. This function is written in C# script, so we have to add a new file to pull in these dependencies. If we were using native C# from Visual Studio, we could simply add in dependencies to these libraries and they would be included when the function is deployed. In the code editor for your function, expand the panel on the right and click View files. Click Add to add a new file and name it project.json, then click it to pull it up in the editor. Add in the following text to define the necessary dependencies:

 {
    "frameworks": {
        "net46": {
            "dependencies": {
                "Microsoft.Azure.KeyVault": "2.4.0-preview",
                "Microsoft.Azure.Services.AppAuthentication": "1.1.0-preview"
            }
        }
    }
}

Go ahead and click save, then pull up run.csx in the editor. Add the following using statements to the file so we can use these libraries.

 using Microsoft.Azure.Services.AppAuthentication;
using Microsoft.Azure.KeyVault;

Then delete the lines that contain your connection string (they start with string sourceStorageConnectionString and string destinationStorageConnectionString) and add the following code in to pull the credentials from key vault, replacing the URL's respectively.

 // Pull the connection string out of Azure Key Vault
string sourceStorageConnectionStringKvUrl = "https://jbkvtest1.vault.azure.net/secrets/sourceStorageConnectionString";
string destinationStorageConnectionStringKvUrl = "https://jbkvtest1.vault.azure.net/secrets/destinationStorageConnectionString";
AzureServiceTokenProvider azureServiceTokenProvider = new AzureServiceTokenProvider();
var keyVaultClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(azureServiceTokenProvider.KeyVaultTokenCallback));
var sourceStorageConnectionStringSecret = await keyVaultClient.GetSecretAsync(sourceStorageConnectionStringKvUrl).ConfigureAwait(false);
string sourceStorageConnectionString = sourceStorageConnectionStringSecret.Value;
var destinationStorageConnectionStringSecret = await keyVaultClient.GetSecretAsync(destinationStorageConnectionStringKvUrl).ConfigureAwait(false);
string destinationStorageConnectionString = destinationStorageConnectionStringSecret.Value;

The values for each of the key vault URLs can be found in your key vault by viewing each secret, and copying the Secret Identifier for each secret. They have the format of https://keyVaultName.vault.azure.net/secrets/secretName/versionIdentifier. You can either specify the exact version with this full URL or by removing the string of characters and numbers you can just get the latest version. In my example I drop the string specifying the version so I can just access the latest version. This makes key rolling easy, as I don't have to update my code to reference a new version of the key.

Save your function and that's it! Now try uploading a file to your primary storage account and you should see it replicate to the secondary, getting the connection strings from the key vault. If you'd like to see the exact values that it's pulling from key vault you can add in a couple of logging entries to output these values.

 log.Info($"source connection string: {sourceStorageConnectionString}");
log.Info($"destination connection string: {destinationStorageConnectionString}");

One thing to note is that every function invocation will require a call to key vault, so it might be beneficial to cache this data to reduce the number of calls to key vault. You could use a MemoryCache to cache it across executions on the same server or leverage another caching technology such as Azure Redis Cache. This would be something to consider if the key vault calls are too expensive from a cost or an execution time standpoint, though it is a pretty inexpensive service.

Wrap up

So we've taken a look at what Managed Service Identity is and how you can use it within Azure Functions to eliminate sensitive credentials in code. You can use this across multiple services and I can't wait for more Azure services to adopt this capability. It's easy to use and enhances the security of your Azure resources. Check out the following links for more information.

Managed Service Identity overview - /en-us/azure/active-directory/msi-overview
Using Managed Service Identity in App Service - /en-us/azure/app-service/app-service-managed-service-identity
GitHub repo with function code - https://github.com/jboeshart/ReplicateBlobAzureFunction/tree/master
Microsoft.Azure.Services.AppAuthentication nuget package - https://www.nuget.org/packages/Microsoft.Azure.Services.AppAuthentication
Microsoft.Azure.KeyVault nuget package - https://www.nuget.org/packages/Microsoft.Azure.KeyVault/2.4.0-preview