Access Policies In Key Vault Using Azure Bicep - How To Create New, Keep Existing, Managed Identity

In this post, we will discuss key vault access policies in detail, i.e. what it is, what is it for, its schema, how to deploy, and some solutions to common use cases, all this in the context of Azure Bicep.

Access policy is an access-control model for Azure Key Vault. This model allows creating access policies which define permissions for different Azure AD security principals over key vault specific scopes (keys, secrets, certificates).

Access policies are still widely used when working with Azure Key Vaults even though newer Azure RBAC model exists. So, it is definitely worth learning more about key vault access policies.

Contents:

Overview

In one of the previous posts Key Vault & Secrets Management With Azure Bicep, we talked about the key vault itself. In this post, we will cover access policies and how to work with them using Azure Bicep.

We start by defining what is an access policy and discussing how it is different from the role-based access control model which is also available in Azure Key Vault.

Next, we cover the resource schema, and in particular talk about tenantId, objectId, permissions, applicationId properties and what are they for.

Creating Access Policies In Bicep section contains examples how to deploy access policies as part of a key vault deployment as well as how to add access policies to an existing key vault.

The last two sections are about possible use cases when working with access policies in Azure Bicep. First one is assigning an access policy for a managed identity, and the second one is a solution for how to redeploy a key vault but keep existing access policies which aren’t defined in the template.

What is Access Policy?

Access policy model is one of the two permission models available for Azure Key Vault. Historically, access policies were introduced earlier than Azure RBAC, and the latter now is the recommended approach.

An access policy specifies what actions a particular security principal (user, group, service principal, or managed identity) is allowed to perform over different scopes (keys, secrets, certificates).

According to the docs, each key vault can have only up to 1024 access policies, so it is possible to hit the limit if permissions are assigned to each user individually rather than to groups of users.

Access Policies vs Role-Based Access Control (RBAC)

As already mentioned, there is an alternative permissions model which is called Azure RBAC. It is widely used across Azure resources and, as a result, provides more uniform experience.

There are pros and cons for both models, but in general, Azure RBAC allows more fine-grained control and is now the recommended way of managing permissions for key vaults. Also, please see comparison in this post Access Policies vs Azure RBAC.

Resource Schema For Access Policy

In Azure Resource Manager, and therefore in Azure Bicep, an access policy is a child resource of a key vault resource. It is also reflected in the resource name: Microsoft.KeyVault/vaults/accessPolicies.

Being a child resource means that an access policy must be created in the context of some key vault, and it cannot exist on its own. Read more about Child Resources In Azure Bicep - 3 Ways To Declare, Loops, Conditions.

The schema with detailed description can be found in the documentation: Microsoft.KeyVault/vaults/accessPolicies. However, we will still give a short summary of each property.

NOTE: When deploying a key vault, the accessPolicies property is required unless the mode is set to “recover” or RBAC is enabled. Sometimes, people want to preserve existing access policies which are not defined in the template, then they resort to work-arounds like Keep Existing Access Policies When Redeploying Key Vault.

// ============ accesspolicies-schema.bicep ============

resource symbolicname 'Microsoft.KeyVault/vaults/accessPolicies@2019-09-01' = {
  name: 'string'
  parent: parentSymbolicName
  properties: {
    accessPolicies: [
      {
        applicationId: 'string'
        objectId: 'string'
        permissions: {
          certificates: ['string']
          keys: ['string']
          secrets: ['string']
          storage: ['string']
        }
        tenantId: 'string'
      }
    ]
  }
}

Creating Access Policies In Bicep

Given that we already know the schema and what values we want to put where, now we need to decide how to organize our Bicep template and deploy our access policies.

In general, I prefer to think about access policies deployment in terms of two categories:

The following sections illustrate the two use cases mentioned above.

[Template With KV] Deploy Access Policies Together With Key Vault

When deploying a key vault using Bicep, accessPolicies property is required and is also a part of the key vault schema Microsoft.KeyVault/vaults.

Therefore, if your template already contains a key vault definition, then the simplest way is to just specify access policies as part of the key vault as shown in the code snippet below.

// ============ keyvault-with-accesspolicies.bicep ============

resource keyVault 'Microsoft.KeyVault/vaults@2019-09-01' = {
  name: 'kv-contoso'
  location: resourceGroup().location
  properties: {
    sku: {
      family: 'A'
      name: 'standard'
    }
    tenantId: '51636b66-283b-4600-8ce6-4ad411dcfb42'
    accessPolicies: [
      {
        objectId: '90589367-f0f1-4289-bbce-ad5bb54cef23'
        tenantId: '51636b66-283b-4600-8ce6-4ad411dcfb42'
        permissions: {
          secrets: [
            'list'
          ]
          certificates: [
            'list'
          ]
        }
      }
      {
        objectId: '2d267b98-52fc-405b-8071-70846363fd40'
        tenantId: '51636b66-283b-4600-8ce6-4ad411dcfb42'
        permissions: {
          secrets: [
            'all'
          ]
          certificates: [
            'get'
            'create'
          ]
          keys: [
            'encrypt'
            'decrypt'
          ]
        }
      }
    ]
  }
}

[Template Without KV] Add Access Policy To Existing Key Vault

Now, let’s assume that we want to deploy access policies separately from their key vault, for example, this could be in the following use cases:

To add access policies, we have to define a resource of a type Microsoft.KeyVault/vaults/accessPolicies.

On line 6, we can see that name consists of two segments separated by a slash / where the first part is the key vault name (e.g. kv-contoso), and the second part is always add. As a result, we get a value kv-contoso/add.

NOTE: An alternative approach is to pass key vault reference in the parent property of the access policies resource, in such case the name property will have only one segment and be just add without the key vault name. See an example in this post about child resources.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ============ add-accesspolicies.bicep ============

param keyVaultName string = 'kv-contoso'

resource accessPolicies 'Microsoft.KeyVault/vaults/accessPolicies@2019-09-01' = {
  name: '${keyVaultName}/add'
  properties: {
    accessPolicies: [
      {
        objectId: '90589367-f0f1-4289-bbce-ad5bb54cef23'
        tenantId: '51636b66-283b-4600-8ce6-4ad411dcfb42'
        permissions: {
          secrets: [
            'all'
          ]
          certificates: [
            'list'
          ]
        }
      }
    ]
  }
}

Managed Identity Access Policy

Managed identity in Azure is a concept that allows offloading of credential management from a developer to the Azure infrastructure. This way, applications can use managed identities provided by Azure to authenticate to Azure Active Directory.

And since Key Vault integrates with Azure AD, managed identities are often used by applications to retrieve secrets/certificates from the key vault. To enable this, managed identity must have relevant permissions assigned to it inside the key vault, and one of the ways to achieve it is to create an access policy.

In Azure, managed identities come in two flavors: user-assigned and system-assigned. In terms of granting permissions through access policies, there is no difference between these two.

NOTES:

// ========== managed-identity-accesspolicy.bicep ==========

resource keyVault 'Microsoft.KeyVault/vaults@2019-09-01' = {
  name: 'kv-contoso'
  location: resourceGroup().location
  properties: {
    sku: {
      family: 'A'
      name: 'standard'
    }
    tenantId: subscription().tenantId
    accessPolicies: [
      {
        objectId: '61248cef-973e-4471-92fb-fe3653b1d804'  // This is the objectId of our managed identity
        tenantId: subscription().tenantId
        permissions: {
          secrets: [
            'get'
          ]
        }
      }
    ]
  }
}

Keep Existing Access Policies When Redeploying a Key Vault

Let’s imagine the following situation:

  1. We want to deploy a key vault which may or may not already exist.
  2. If it exists then we want to redeploy it but keep existing access policies.
  3. If it doesn’t exist, we just create it with an empty or default set of access policies.

Unfortunately, there is no straightforward way in Azure Bicep or ARM templates to check whether a resource already exists or not, though it would be very helpful in this situation.

Therefore, we need to use a work-around, and here are is a possible approach:

  1. If we have knowledge about whether the key vault exists or not when the deployment is performed, we can use reference() function to retrieve access policies of the existing key vault during deployment.
  2. If we don’t know whether the key vault exists, then we can use Azure PowerShell or Azure CLI to check the key vault existence and then use the template from the previous step to deploy the key vault.

1. Get Existing Access Policies Using Reference() Function

For now, let’s assume that we know whether a key vault already exists or not, this information is somehow passed to the template through a boolean flag, e.g. isNewKeyVault.

Given this information, we can now easily pass necessary access policies when defining a key vault. We will use ARM template reference() function to achieve this.

Our Azure Bicep deployment will consist of two files, here is an overview for each of them:

// ========== keyvault-existing-accesspolicies.bicep ==========

param keyVaultName string = 'kv-contoso'
param isNewKeyVault bool = false

// If it's a new key vault, we pass empty array but it could be any static set of access policies
var accessPolicies = isNewKeyVault ? [] : reference(resourceId('Microsoft.KeyVault/vaults', keyVaultName), '2019-09-01').accessPolicies

module keyVaultModule 'keyvault-existing-accesspolicies.module.bicep' = {
  name: 'keyVaultDeployment'
  params: {
    keyVaultName: keyVaultName
    accessPolicies: accessPolicies
  }
}
// ========== keyvault-existing-accesspolicies.module.bicep ==========
// This is a module which is invoked from 'keyvault-existing-accesspolicies.bicep'

param keyVaultName string
param accessPolicies array  // Expecting access policies to be passed through this parameter

resource keyVault 'Microsoft.KeyVault/vaults@2019-09-01' = {
  name: keyVaultName
  location: resourceGroup().location
  properties: {
    sku: {
      family: 'A'
      name: 'standard'
    }
    tenantId: subscription().tenantId
    accessPolicies: accessPolicies
  }
}

2. Use Azure PowerShell To Check Key Vault Existence

The approach in the previous section works well if the template knows whether a new key vault is being created or it’s an existing one.

We can easily find out whether a key vault already exists using Azure PowerShell / Azure CLI / REST API, and then pass this knowledge to the template to make our solution be able to handle both cases.

NOTE: In this example, we are using Azure PowerShell to check the existence of Azure Key Vault.

So, the high-level steps are:

  1. Check whether the key vault exists, for example, using Get-AzKeyVault function.
    This should look similar to the following:
    $KeyVault = Get-AzKeyVault -VaultName kv-contoso -ResourceGroupName rg-contoso
    $IsNewKeyVault = $KeyVault -eq $null
    
  2. Deploy keyvault-existing-accesspolicies.bicep template from the previous step using New-AzResourceGroupDeployment. An example how to do that is available in 5 Ways To Deploy Bicep File With Parameters - Azure DevOps, PowerShell, CLI, Portal, Cloud Shell.

And this is basically it! Now you should be able to deploy a new or existing key vault and preserve existing access policies if there are any.

If you are interested how to implement this in CI/CD pipeline, I’d recommend checking out the post Deploy Azure Bicep In YAML and Classic Release Pipelines (CI/CD) - Azure DevOps.