Menu

Lessons learned: Assigning Access Policies to KeyVault with Bicep

Example architecture

Chart below describes a quite typical architecture where Microservice resources and KeyVault have own resource groups. KeyVault is typically located in own resource group because it's often considered as an environment specific resource which 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 interactive with multiple resource groups. 

undefined

Bicep implementation

App Service module

This module creates App Service, enables Managed Identity and sets same preferred settings. Module returns principal Id of App Service when 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

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 App Service Module to create 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 Main Bicep file 

I first created reference to 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 to the same Resource Group

2. 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 like in KeyVault resource declaration. Visual Studio Code showed immediately 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".

undefined

That warning was a 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 that we can configure different resource group for module than which 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