Lessons learned: Assigning Access Policies to KeyVault with Bicep

Example architecture

The chart below describes a quite typical architecture where Microservice resources and KeyVault have their own resource groups. KeyVault is typically located in its own resource group because it's often considered as an environment-specific resource that is used by multiple resources/services. This blog post covers what I learned when deploying this kind of infrastructure as a code with Bicep when you have to interact with multiple resource groups.

Bicep implementation


App Service module

This module creates an App Service, enables Managed Identity, and sets the same preferred settings. The module returns the principal Id of App Service when the resource is created.

@description('Name of the App Service Plan')
param appServicePlanName string

@description('The SKU of App Service Plan.')
param appServicePlanSku string = 'S1'

@allowed([
  'Win'
  'Linux'
])
@description('Select the OS type to deploy.')
param appServicePlanPlatform string

@description('Name of the App Service')
param appServiceName string
param tags object
param appSettings array

resource appServicePlan 'Microsoft.Web/serverfarms@2021-02-01' = {
  name: appServicePlanName
  location: resourceGroup().location
  sku: {
    name: appServicePlanSku
  }
  kind: ((appServicePlanPlatform == 'Linux') ? 'linux' : 'windows')
  tags:tags
}

resource appService 'Microsoft.Web/sites@2021-02-01' = {
  name: appServiceName
  location: resourceGroup().location
  identity: {
     type: 'SystemAssigned'
  }
  properties:{
    serverFarmId: appServicePlan.id
    siteConfig:{
      alwaysOn: true
      ftpsState: 'Disabled'
      netFrameworkVersion: 'v6.0'
      ipSecurityRestrictions:[
        {
            ipAddress: 'xxx.xxx.xxx.xxx/32'
            action: 'Allow'
            tag: 'Default'
            priority: 100
            name: 'Allow Access from my IP'
        }
        {
          ipAddress: 'Any'
          action: 'Deny'
          priority: 2147483647
          name: 'Deny all'
          description: 'Deny all access'
        }
      ]
      scmIpSecurityRestrictionsUseMain: true
      appSettings: appSettings
    }
    httpsOnly: true    
  }
  tags:tags
}

output principalId string = appService.identity.principalId

Main Bicep file

The main file orchestrates the creation of infrastructure.

Parameter declaration

@description('Name of the App Service Plan')
param appServicePlanName string

@description('Name of the Application Insights')
param applicationInsightsName string

@description('The SKU of App Service Plan.')
param appServicePlanSku string = 'S1'

@allowed([
  'Win'
  'Linux'
])
@description('Select the OS type to deploy.')
param appServicePlanPlatform string

@description('Name of the App Service')
param appServiceName string

Utilize the App Service Module to create an App Service

module appService './appservice.bicep' = {
  name: 'appService'
  params: {
      appServiceName: appServiceName
      appServicePlanName: appServicePlanName
      appServicePlanPlatform: appServicePlanPlatform
      appServicePlanSku: appServicePlanSku
      tags:defaultTags
      appSettings: appSettings
  }
}

Now Managed Identity enabled App Service is created. Next access policies to KeyVault should be created to enable App Service communication with KeyVault.

Failed tryouts

  1. Referencing existing KeyVault in the Main Bicep file

I first created a reference to the existing KeyVault like this. With scope, I explicitly specified the resource group of KeyVault.

resource keyVault 'Microsoft.KeyVault/vaults@2019-09-01' existing = {
  name: keyVaultResourceName
  scope: resourceGroup(subscription().subscriptionId, 'rg-keyvault') 
}

KeyVault reference seems to be working fine. Access policies of KeyVault should be added next. I found several examples to assign access policies like this:

resource keyVaultPolicies 'Microsoft.KeyVault/vaults/accessPolicies@2021-06-01-preview' = {
  dependsOn:[
    keyVault
  ]
  name: '${keyVault.name}/add'
  properties: {    
    accessPolicies: [
      {
        objectId: appService.outputs.principalId
        permissions: {
          secrets: [ 
            'get'
          ]
        }
        tenantId: subscription().tenantId
      }
    ]
  }
}  

Everything looks good so let's deploy infrastructure with the following Azure CLI command

NOTE: 'rg-bicep' is a Resource Group where App Service will be created. KeyVault is not located in that Resource Group!

az deployment group create --resource-group rg-bicep --template-file main.bicep --parameters parameters/dev.parameters.json

This didn't work because the following error occurred:

{
    "status": "Failed",
    "error": {
        "code": "ParentResourceNotFound",
        "message": "Can not perform requested operation on nested resource. Parent resource 'MYKEYVAULTRESOURCE' not found."
    }
}

Seems that KeyVault is not found. KeyVault is tried to find from the same Resource Group where App Service is located.

NOTE: This is working if KeyVault is located in the same Resource Group

  1. Changed scope of Access Policy resource declaration in Main Bicep file

Next, I tried to add scope to the Access Policy resource declaration similar way to in KeyVault resource declaration. Visual Studio Code shows immediately a warning that this is not allowed: "The root resource scope must match that of the Bicep file. To deploy a resource to a different root scope, use a module".

That warning was really good because it revealed that this is not the right way to handle this kind of scenario.

Refactored solution


KeyVault Policy Module

Handling of KeyVault Access Policy is now separated to the own Bicep Module (keyvaultpolicy.bicep)

@description('Name of the KeyVault resource ex. kv-myservice.')
param keyVaultResourceName string
@description('Principal Id of the Azure resource (Managed Identity).')
param principalId string
@description('Assigned permissions for Principal Id (Managed Identity)')
param keyVaultPermissions object
@allowed([
  'add'
  'remove'
  'replace'
])
@description('Policy action.')
param policyAction string

resource keyVault 'Microsoft.KeyVault/vaults@2019-09-01' existing = {
  name: keyVaultResourceName
  resource keyVaultPolicies 'accessPolicies' = {
    dependsOn:[
      keyVault
    ]  
    name: policyAction
    properties: {    
      accessPolicies: [
        {
          objectId: principalId
          permissions: keyVaultPermissions
          tenantId: subscription().tenantId
        }
      ]
    }
  }  
}


Main Bicep file

Scope configuration of the Module enables us to configure different resource groups for the module than is used in the main Bicep file context.

var keyVaultPermissions = {
  secrets: [ 
    'get'
  ]
}

module keyVault './keyvaultpolicy.bicep' = {
  dependsOn: [
    appService
  ]
  scope: resourceGroup('rg-keyvault')
  name: 'keyVault'
  params: {
      keyVaultResourceName: keyVaultResourceName
      principalId: appService.outputs.principalId
      keyVaultPermissions: keyVaultPermissions
      policyAction: 'add'
  }
}

This solved the problem!

Comments