Referencing Azure Key Vault secrets in Azure Functions
Janne Kemppainen |In the previous posts we’ve covered how to create an Azure Key Vault using Bicep and how to create an Azure Function App using Bicep. Now, let’s see how we can combine them together and reference secrets from a Key Vault in an Azure Function.
If you haven’t read the previous posts, I highly recommend you do so before continuing. They will help you understand the concepts we’ll be covering in this post and make it easier to follow along. However, if you’re already familiar with the topics, you can continue reading.
Introduction
When you need to store sensitive information, such as connection strings, passwords, or certificates in your function app, you have two options:
- Use the function app’s application settings.
- Store the information in an Azure Key Vault and reference it from the function app.
The first option is the simplest one, but it’s not the most secure. Even though the application settings are encrypted at rest, they are still stored in the function app’s configuration. This means that anyone with access to the function app can access the secrets.
The configuration is also more prone to errors, as you can accidentally overwrite the settings, in which case you’ll have to be able to restore the secrets from somewhere else.
Storing the secrets in a Key Vault is the recommended approach. It’s more secure, as the secrets are stored in a separate resource whose only purpose is to store sensitive information. The access policies allow you to control who has access, even on a per-secret basis. The Key Vault also has a soft-delete feature, which allows you to recover accidentally deleted secrets.
A sort of middle ground between the two options would be to store the secrets in the function app’s configuration, but to populate them from a Key Vault during deployment. The main drawbacks are still there, but at least you have an automated way to restore the secrets if they are accidentally overwritten.
Accessing Key Vault secrets from Azure Functions
Assuming that you already have a Key Vault and a function app, and that the needed permissions are in place, let’s see how we can reference the secrets from the function app. Don’t worry if you don’t have these setup yet as we’ll go through the steps in the next sections.
Key Vault references are stored in the function app’s application settings just like other configurations, but they use a special syntax that the function app runtime understands.
There are two ways to write the secret references:
@Microsoft.KeyVault(SecretUri=secretUri)
@Microsoft.KeyVault(VaultName=vaultName;SecretName=secretName;SecretVersion=secretVersion)
They are essentially two different ways to say the same thing. The first one uses the secret URI, which is the full URL to the secret, while the other one specifies the various parts of the secret URI separately.
The secret URI can be in one of the following formats:
https://<vault-name>.vault.azure.net/secrets/<secret-name>/<secret-version>
https://<vault-name>.vault.azure.net/secrets/<secret-name>/
As you can see, the version is optional and if it’s not specified, the latest version of the secret is used. Note that the second URI has a trailing slash, which is required.
The SecretVersion
parameter is also optional for the second syntax and can be omitted to use the latest version of the secret.
When the function app starts, it will fetch the secrets from the Key Vault and make them available to the application code. Successful population of a secret is shown in the application settings as a Key Vault reference with a green checkmark. If the secret can’t be populated for some reason it will show a red cross instead.
What happens when a secret is updated?
When a secret is updated in the Key Vault, it can take up to 24 hours for the change to be reflected in the function app. This is because they are cached for performance reasons.
If you need to update secrets immediately, you can restart the function app, which will clear the cache and fetch the secrets again.
One way to do this is to use the Azure CLI:
az functionapp restart --name <function-app-name> --resource-group <resource-group-name>
Alternatively, you can use the Azure Portal. Navigate to the function app and click on the Restart button in the top menu.
Resource definitions with Bicep
Now that we know how to reference secrets from a Key Vault in an Azure Function, let’s see how we can define the resources with Bicep. You can find more details about the resources in the previous posts for Key Vault and Function Apps. We’ll only cover the parts that are specific to referencing secrets from a Key Vault.
Key Vault
As we saw in the previous post, the Key Vault resource definition is pretty straightforward. I’ve simplified it a bit by removing some of the parameters and variables as they are not relevant for the topic this post.
@description('Are you creating a new Key Vault?')
param createKeyVault bool = false
@description('Resource group name, use default value')
param resourceGroupName string = resourceGroup().name
@description('Resource location, use default value from resource group location')
param location string = resourceGroup().location
@description('Specifies the Azure Active Directory tenant ID that should be used for authenticating requests to the key vault.')
param tenantId string = subscription().tenantId
var createMode = createKeyVault ? 'default' : 'recover'
resource kv 'Microsoft.KeyVault/vaults@2023-02-01' = {
name: 'kv-${uniqueString(resourceGroupName)}'
location: location
properties: {
sku: {
family: 'A'
name: 'standard'
}
createMode: createMode
tenantId: tenantId
accessPolicies: []
}
tags: tags
}
The createKeyVault
parameter is again used to determine whether we are creating a new Key Vault or recovering an existing one. Trying to recover a non-existing Key Vault as well as trying to create an existing one again will both result in an error. So make sure that you set the parameter to true only on the first deployment before the Key Vault exists.
The Key Vault name is autogenerated based on the resource group name but this can be changed to any globally unique value. The location is also taken from the resource group.
Note that the accessPolicies
property is an empty array. We’ll add the access policies in the next section with a separate resource definition.
Function App
Now, let’s add the function app resource definition in the same file. The function app resource definition is also pretty straightforward but it too has been simplified from the previous post. This time we’ll skip the creation of a separate deployment slot and application insights. We’ll also ignore the additional app settings and tags.
The changed lines are highlighted below.
@description('Are you creating a new Key Vault?')
param createKeyVault bool = false
@description('Resource group name, use default value')
param resourceGroupName string = resourceGroup().name
@description('Resource location, use default value from resource group location')
param location string = resourceGroup().location
@description('Specifies the Azure Active Directory tenant ID that should be used for authenticating requests to the key vault.')
param tenantId string = subscription().tenantId
var createMode = createKeyVault ? 'default' : 'recover'
var functionAppName = 'azfunctions-${uniqueString(resourceGroupName)}'
var storageAccountName = '${uniqueString(resourceGroup().id)}azfunctions'
var functionAppSettings = [
{
name: 'DB_USERNAME'
value: '@Microsoft.KeyVault(VaultName=${kv.name};SecretName=db-username)'
}
{
name: 'DB_PASSWORD'
value: '@Microsoft.KeyVault(VaultName=${kv.name};SecretName=db-password)'
}
{
name: 'AzureWebJobsStorage'
value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}'
}
{
name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING'
value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}'
}
{
name: 'WEBSITE_CONTENTSHARE'
value: toLower(functionAppName)
}
{
name: 'FUNCTIONS_EXTENSION_VERSION'
value: '~4'
}
{
name: 'WEBSITE_NODE_DEFAULT_VERSION'
value: '~18'
}
{
name: 'FUNCTIONS_WORKER_RUNTIME'
value: 'node'
}
{
name: 'WEBSITE_RUN_FROM_PACKAGE'
value: '1'
}
]
resource kv 'Microsoft.KeyVault/vaults@2023-02-01' = {
name: 'kv-${uniqueString(resourceGroupName)}'
location: location
properties: {
sku: {
family: 'A'
name: 'standard'
}
createMode: createMode
tenantId: tenantId
accessPolicies: []
}
tags: tags
}
resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = {
name: storageAccountName
location: location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
}
resource hostingPlan 'Microsoft.Web/serverfarms@2022-03-01' = {
name: 'FunctionApps'
location: location
sku: {
name: 'Y1'
tier: 'Dynamic'
}
}
resource functionApp 'Microsoft.Web/sites@2022-03-01' = {
name: functionAppName
location: location
kind: 'functionapp'
identity: {
type: 'SystemAssigned'
}
properties: {
serverFarmId: hostingPlan.id
siteConfig: {
appSettings: functionAppSettings
ftpsState: 'Disabled'
minTlsVersion: '1.2'
http20Enabled: true
}
httpsOnly: true
}
}
resource accessPolicies 'Microsoft.KeyVault/vaults/accessPolicies@2022-07-01' = {
name: 'add'
parent: kv
properties: {
accessPolicies: [
{
objectId: functionApp.identity.principalId
tenantId: functionApp.identity.tenantId
permissions: {
secrets: [
'get'
]
}
}
]
}
}
The function app settings are now defined in the functionAppSettings
variable. It contains two values that reference secrets from the Key Vault that is created in the same file. The vault name is referenced with the kv.name
expression and the secret name is hardcoded in both cases.
The added resources are needed by the function app. It requires a storage account for storing the function app code and a hosting plan for running the function app. This configuration is essentially the same as in the previous post, minus the deployment slot and application insights. Note that the function app is configured to use a system-assigned managed identity.
The accessPolicies
resource definition is also new. It defines an access policy that allows the function app to read secrets from the Key Vault. The objectId
and tenantId
properties are used to identify the function app’s managed identity. The permissions
property defines the permissions that the function app has on the Key Vault. In this case, it only has the get
permission for secrets.
Deploying the infrastructure
Let’s deploy the infrastructure with the az deployment group create
command. The command requires the resource group name and the template file as parameters. Since the Key Vault doesn’t exist yet, we’ll set the createKeyVault
parameter to true.
az deployment group create \
--resource-group functions-infra \
--template-file functions-infra.bicep \
--parameters createKeyVault=true
After the deployment is complete, you should see the key vault, function app and other resources in the resource group.
Creating secrets in the Key Vault
Now that we have the infrastructure in place, let’s create the secrets. We’ll create two secrets, one for the database username and one for the password. The secrets are created with the az keyvault secret set
command. The command requires the vault name, secret name, and the secret value as parameters. The secret value can be passed as a parameter or read from a file. We’ll use both methods in this example.
Right now, the function app can read secrets from the Key Vault, but you don’t have access to create secrets. Therefore, you’ll need to add yourself with an access policy to the Key Vault. First, you’ll need to get your object ID az account show
command.
az account show --query id
Then you can add yourself to the Key Vault with the az keyvault set-policy
command. The command requires the vault name, your object ID, and the permissions as parameters. The permissions can be specified as a space-separated list of values or as a JSON string.
az keyvault set-policy \
--name <your-vault-name> \
--object-id <your-object-id> \
--secret-permissions get list set delete backup recover restore purge
Now you should be able to create secrets in the Key Vault.
Alternatively, you could add your user account to the access policies in the Bicep file and redeploy the infrastructure. It is also possible to edit the access policies in the Azure Portal.
Create a file named db-password.txt
with the password as the content. Then run the following commands to create the secrets, replacing the vault name and secret values with your own.
az keyvault secret set \
--vault-name kv-functions-infra \
--name db-username \
--value dbuser
az keyvault secret set \
--vault-name kv-functions-infra \
--name db-password \
--file db-password.txt
Now that the secrets are in place, you should see them appear in the function app’s application settings in the Azure Portal with a green checkmark. The values are available to the function app code as environment variables but we’re not going to cover deploying a function app in this post.
Summary
In this post, we saw how to reference secrets from a Key Vault in an Azure Function. We also saw how to define the resources with Bicep. The function app resource definition was simplified a bit from the previous post, but the Key Vault resource definition was essentially the same.
Hopefully, this post gave you a better understanding of secrets in Azure Functions. See you in the next post!
Previous post
Using GitHub Copilot for writing documentation