Coder Perfect

The Right Way to Manage Identity and Key Vault with App Service

Problem

I’m now attempting to deploy a resource group using Azure Bicep, however I’m having trouble using key vault for my Azure app service. I’d like to know if I’m doing things correctly. I have a main biceps file that looks like this:

// params removed for brevity...

targetScope = 'subscription'

resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = {
  name: 'rg-${appName}-${region}'
  location: 'centralus'
}

module appServicePlan 'appplan.bicep' = {
  params: {
    sku: appServicePlanSku
    appName: appName
    region: region
  }
  scope: rg
  name: 'AppServicePlanDeploy'
}

module keyVault 'keyvault.bicep' = {
  params: {
    keyVaultName: keyVaultName
    sqlPassword: sqlServerPassword
    webSiteManagedId: webSite.outputs.webAppPrincipal
  }
  scope: rg
  name: 'KeyVaultDeploy'
  dependsOn: [
    webSite
  ]
}

module ai 'ai.bicep' = {
  scope: rg
  name: 'ApplicationInsightsDeploy'
  params: {
    name: appName
    region: region
    keyVaultName: keyVault.outputs.keyVaultName
  }
  dependsOn: [
    keyVault
  ]
}

resource kv 'Microsoft.KeyVault/vaults@2019-09-01' existing = {
  name: keyVaultName
  scope: rg
}

module sql 'sqlserver.bicep' = {
  scope: rg
  name: 'SQLServerDeploy'
  params: {
    appName: appName
    region: region
    sqlPassword: kv.getSecret('sqlPassword')
    sqlCapacitity: sqlCapacitity
    sqlSku: sqlSku
    sqlTier: sqlTier
  }
  dependsOn: [
    keyVault
  ]
}

module webSite 'site.bicep' = {
  params: {
    appName: appName
    region: region
    keyVaultName: keyVaultName
    serverFarmId: appServicePlan.outputs.appServicePlanId
  }
  scope: rg
  name: 'AppServiceDeploy'
  dependsOn: [
    appServicePlan
  ]
}

My question concerns the implementation of site.bicep. I started by passing the secret uri from exported variables and creating the web app last because app insights, sql, and other keyvault-enabled components must all be setup and in keyvault before we can use their exported secret uri to build a config. Before, I had something like this: site.bicep (before):

  properties: {
    serverFarmId: serverFarmId
    keyVaultReferenceIdentity: userAssignedId
    siteConfig: {
      appSettings: [
        {
          name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
          value: '@Microsoft.KeyVault(SecretUri=${appInsightsConnectionString})'
        }
        {
          name: 'APPINSIGHTS_INSTRUMENTATIONKEY'
          value: '@Microsoft.KeyVault(SecretUri=${appInsightsKey})'
        }
      ]
      netFrameworkVersion: 'v5.0'
    }

}

The only issue with this design is that the key vault must be built before the website because sql, ai, and the other services will store their data in the key vault for consumption by the web app via their respective uris. The problem is that KeyVault has no notion which Azure service it should allow access to its keys.

Is the solution of building the web app first and then the key vault the only way to solve this problem? On the web app, I’m using managed IDs and would like to keep doing so if possible. In the end, I came up with something like this:

site.bicep (final)

// params removed for brevity...
resource webApplication 'Microsoft.Web/sites@2020-12-01' = {
  name: 'app-${appName}-${region}'
  location: resourceGroup().location
  tags: {
    'hidden-related:${resourceGroup().id}/providers/Microsoft.Web/serverfarms/appServicePlan': 'Resource'
  }
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    serverFarmId: serverFarmId
    siteConfig: {
      appSettings: [
        {
          name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
          value: '@Microsoft.KeyVault(SecretUri=${keyVaultName}.vault.azure.net/secrets/aiConnectionString)'
        }
        {
          name: 'APPINSIGHTS_INSTRUMENTATIONKEY'
          value: '@Microsoft.KeyVault(SecretUri=${keyVaultName}.vault.azure.net/secrets/aiInstrumentationKey)'
        }
        {
          name: 'AngularConfig:ApplicationInsightsKey'
          value: '@Microsoft.KeyVault(SecretUri=${keyVaultName}.vault.azure.net/secrets/aiInstrumentationKey)'
        }
      ]
      netFrameworkVersion: 'v5.0'
    }
  }
}

output webAppPrincipal string = webApplication.identity.principalId

And there’s the KeyVault, which will require a webSite that depends on it.

keyVault.bicep(final):

resource keyVault 'Microsoft.KeyVault/vaults@2019-09-01' = {
  name: keyVaultName
  location: resourceGroup().location
  properties: {
    enabledForDeployment: true
    enabledForTemplateDeployment: true
    enabledForDiskEncryption: true
    enableRbacAuthorization: true
    tenantId: subscription().tenantId
    sku: {
      name: 'standard'
      family: 'A'
    }
    accessPolicies: [
      {
        tenantId: subscription().tenantId
        objectId: webSiteManagedId
        permissions: {
          keys: [
            'get'
          ]
          secrets: [
            'list'
            'get'
          ]
        }
      }
    ]
  }
}

Asked by MrEvanJ

Solution #1

Simply treat your accessPolicies as a separate resource and include them when you build both the Key Vault and the App Service. The same is true for the Configuration section and Connection Strings. Here is a link to the paperwork.

Nested templates in ARM templates can achieve the same effect. It’s similar in Bicep, except you describe them as independent resources with parent names (for example, name: ‘$kv.name/add’, name: ‘$webSite.name/connectionstrings’).

Sample

Step 1: Create an App Service that does not have a configuration component.

 resource webSite 'Microsoft.Web/sites@2020-12-01' = {
      name: webSiteName
      location: location
      properties: {
        serverFarmId: hostingPlan.id
        siteConfig:{
          netFrameworkVersion: 'v5.0'
        }
      }
      identity: {
        type:'SystemAssigned'
      }
    }

Step 2: Make a Key Vault with no access restrictions.

resource kv 'Microsoft.KeyVault/vaults@2019-09-01' = {
  name: keyVaultName
  location: location
  properties:{
    sku:{
      family: 'A'
      name: 'standard'
    }
    tenantId: tenantId
    enabledForTemplateDeployment: true
    accessPolicies:[
    ]
  }
}

Step 3: Make a new access policy and use it as a reference. Managed Identity for Web Apps

resource keyVaultAccessPolicy 'Microsoft.KeyVault/vaults/accessPolicies@2021-06-01-preview' = {
  name: '${kv.name}/add'
  properties: {
      accessPolicies: [
          {
              tenantId: tenantId
              objectId: webSite.identity.principalId
              permissions: {
                keys: [
                  'get'
                ]
                secrets: [
                  'list'
                  'get'
                ]
              }
          }
      ]
  }
}

Step 4: Make changes to the Webb app’s configuration area.

resource webSiteConnectionStrings 'Microsoft.Web/sites/config@2020-06-01' = {
  name: '${webSite.name}/connectionstrings'
  properties: {
    DefaultConnection: {
      value: '@Microsoft.KeyVault(SecretUri=${keyVaultName}.vault.azure.net/secrets/aiConnectionString)'
      type: 'SQLAzure'
    }
  }
}

Answered by Serghei Grajdean

Solution #2

Instead of using System Assigned Identity, one approach could be to use User Assigned Identity. Then you’d use the following tactics:

Assigning a user is independent of the resources, avoiding the chicken and egg dilemma.

More:

Answered by Sven

Solution #3

Three aspects of your deployment should be separated into independent resources:

Answered by silent

Post is based on https://stackoverflow.com/questions/69611530/app-service-managed-identity-and-key-vault-the-right-way