Home > front end >  App Service Managed Identity and Key Vault the right way
App Service Managed Identity and Key Vault the right way

Time:10-19

I am currently trying to deploy out a resource group using azure bicep, however, I am running into an issue using key vault for my azure app service. I would like to know if I am actually doing this the correct way. I have a main bicep file that is along the lines of:

// 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 comes with the implementation of the site.bicep, I started off by passing the secret uri from exported variables and creating the web app last as app insights, sql, etc... all need to be setup and in keyvault before we use their exported secret uri to construct a config. I had something along the lines of: 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 problem with this implementation is that the key vault MUST be constructed before the website because sql, ai, and the other services will store their values inside of the key vault for the web app to consume by their respective uris. The issue with this is that the KeyVault rightfully so has no idea which azure service to let access it's keys.

My question is the solution of constructing the web app before the key vault the only way to beat this problem? I am using managed identities on the web app and would like to continue doing so if possible. My final solution ended up somewhat 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 the KeyVault which will take a dependsOn webSite

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'
          ]
        }
      }
    ]
  }
}

CodePudding user response:

One solution could be to use User Assigend Identity instead of System Assigned. Then you would deploy the following:

  1. Deploy a user assigend identity
  2. Key Vault and assign permissions for user assigned identity
  3. Deploy web app with user assigned identity and read / write secrets

User assigned is independent of the resources and so you avoid your chicken and egg problem.

More:

CodePudding user response:

You should split up three parts of your deployment into separate resources:

  1. Deploy the Key Vault - without any access policies!
  2. Deploy the App Service - with the SystemAssigned Identity, but without the app settings
  3. Deploy the Key Vault Access Policy for the MSI
  4. Deploy the App Settings

CodePudding user response:

Just treat your accessPolicies as separate resource and add them when both Key Vault and App Service are created. Same applies for Config section and Connection Strings. Check documentation here.

In ARM templates you can achieve same effect using nested templates. In Bicep it is kind the same, but you declare them as separate resource that usually contains parent name (e.g. name: '${kv.name}/add', name: '${webSite.name}/connectionstrings')

Sample

Step 1: Create an App Service without config section

 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: Create Key Vault without access policies

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: Create new access policy and refference Web Apps Managed Identity

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: Update Webb app config section

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'
    }
  }
}
  • Related