Azure ARM 模板 Keyvault 资源不断删除其他访问策略

Azure ARM Template Keyvault Resources keeps removing other access policies

我创建了一个 ARM 模板来部署一个 Azure WebApp,它使用托管服务身份验证和 KeyVault 来获取机密。因此,ARM 模板创建了 WebApp 资源并启用了 MSI,还创建了 KeyVault 资源并将 WebApp tenantid 和 objectid 添加到 accessPolicies,但是,ARM 模板还从我的 Keyvault 中删除了所有其他现有的访问策略。

有没有一种方法可以增量部署访问策略,这样我就不必在部署后将用户添加回 KeyVault 访问策略?

{
  "type": "Microsoft.KeyVault/vaults",
  "name": "[parameters('ICMODSKeyVaultName')]",
  "apiVersion": "2016-10-01",
  "location": "[resourceGroup().location]",
  "properties": {
    "sku": {
      "family": "A",
      "name": "standard"
    },
    "tenantId": "[reference(variables('identityResourceId'), '2015-08-31-PREVIEW').tenantId]",
    "accessPolicies": [
      {
        "tenantId": "[reference(variables('identityResourceId'), '2015-08-31-PREVIEW').tenantId]",
        "objectId": "[reference(variables('identityResourceId'), '2015-08-31-PREVIEW').principalId]",
        "permissions": {
          "secrets": [
            "get"
          ]
        }
      }
    ],
    "enabledForDeployment": true,
    "enabledForTemplateDeployment":  true
  },
  "dependsOn": [
    "[concat('Microsoft.Web/sites/', parameters('AppName'))]"
  ]
}

这是您应该得到的行为,因为 ARM 模板是幂等的。如果您将访问策略创建为单独的资源,您将能够改变此行为:

{
  "name": "vaultName/policyName",
  "location": xxx,
  "api-version": "2016-10-01",
  "type": "Microsoft.KeyVault/vaults/accessPolicies",
  "properties": {
    "accessPolicies": [
      {
        "tenantId": "00000000-0000-0000-0000-000000000000",
        "objectId": "00000000-0000-0000-0000-000000000000",
        "permissions": {
          "keys": [
            "encrypt"
          ],
          "secrets": [
            "get"
          ],
          "certificates": [
            "get"
          ]
        }
      }
    ]
  }
}

请记住,这是一个粗略的草图,它可能行不通,但您可以很容易地让它工作。这是为了说明这个想法。

参考:https://docs.microsoft.com/en-us/rest/api/keyvault/vaults/updateaccesspolicy

使用 KeyVault 的 apiVersion 2019-09-01 you can workaround this issue by deploying the vault (type: Microsoft.KeyVault/vaults) only when it's new (using condition)。

然后可以使用类型 Microsoft.KeyVault/vaults/accesspolicies

将访问策略单独定义为子资源

我们按照以下变通方案进行幂等密钥保管库 ARM 部署。

1.Deploying 用作检查点的资源组标记。 在参数块中

"resourceTags": {
  "type": "object",
  "defaultValue": {
    "DeploymentLabel": "1"
  }
}

在资源块中

{
  "name": "default",
  "type": "Microsoft.Resources/tags",
  "apiVersion": "2020-10-01",
  "properties": {
    "tags": "[parameters('resourceTags')]"
  }
}
  1. 现在可以使用条件部署,如果只有一个标签:

这意味着只创建了 Resource 组,但尚未创建 Keyvault

{
  "apiVersion": "2019-09-01",
  "name": "[parameters('keyVaultName')]",
  "location": "[resourceGroup().location]",
  "condition": "[equals(variables('tagslength'), 1)]",
  "type": "Microsoft.KeyVault/vaults",
 }

在同一个 keyvault arm 模板中,添加新标签以注册 Keyvault 部署。现在,RG 中添加了 2 个标签,因此我们可以在不丢失访问策略的情况下重新部署。

的问题是它从 ARM 模板中完全删除了密钥保管库,这意味着密钥保管库的创建在新环境中变成了手动过程。

ARM 不允许在不清除其现有访问策略的情况下重新部署密钥保管库。 accessPolicies 属性 自 2018 年起 required (except when recovering a deleted vault), so omitting it will cause an error. Setting it to [] will clear all existing policies. There has been a Microsoft Feedback request 解决此问题,目前有 152 票。

我发现解决此问题的最佳方法是仅在密钥保管库不存在时才有条件地部署密钥保管库,通过单独的add 子资源。这会导致添加或更新指定的策略,同时保留任何其他现有策略。我通过将现有资源名称列表传递给 ARM 模板来检查密钥保管库是否已存在。

在 Azure 管道中:

- task: AzurePowerShell@5
  displayName: 'Get existing resource names'
  inputs:
    azureSubscription: '$(armServiceConnection)'
    azurePowerShellVersion: 'LatestVersion'
    ScriptType: 'InlineScript'
    Inline: |      
      $resourceNames = (Get-AzResource -ResourceGroupName $(resourceGroupName)).Name | ConvertTo-Json -Compress
      Write-Output "##vso[task.setvariable variable=existingResourceNames]$resourceNames"
    azurePowerShellVersion: 'LatestVersion'

- task: AzureResourceManagerTemplateDeployment@3
  name: DeployResourcesTemplate
  displayName: 'Deploy resources through ARM template
  inputs:
    deploymentScope: 'Resource Group'
    action: 'Create Or Update Resource Group'
    # ...
    overrideParameters: >-
      -existingResourceNames $(existingResourceNames)
      # ...
    deploymentMode: 'Incremental'

在 ARM 模板中:

{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",

  "parameters": {
    "keyVaultName": {
      "type": "string"
    },
    "existingResourceNames": {
      "type": "array",
      "defaultValue": []
    }
  },

  "resources": [
    {
      "type": "Microsoft.KeyVault/vaults",
      "apiVersion": "2016-10-01",
      "name": "[parameters('keyVaultName')]",
      "location": "[resourceGroup().location]",
      // Only deploy the key vault if it does not already exist.
      // Conditional deployment doesn't cascade to child resources, which can be deployed even when their parent isn't.
      "condition": "[not(contains(parameters('existingResourceNames'), parameters('keyVaultName')))]",
      "properties": {
        "sku": {
          "family": "A",
          "name": "Standard"
        },
        "tenantId": "[subscription().tenantId]",
        "enabledForDeployment": false,
        "enabledForDiskEncryption": false,
        "enabledForTemplateDeployment": true,
        "enableSoftDelete": true,
        "accessPolicies": []
      },
      "resources": [
        {
          "type": "accessPolicies",
          "apiVersion": "2016-10-01",
          "name": "add",
          "location": "[resourceGroup().location]",
          "dependsOn": [
            "[parameters('keyVaultName')]"
          ],
          "properties": {
            "accessPolicies": [
              // Specify your access policies here.
              // List does not need to be exhaustive; other existing access policies are preserved.
            ]
          }
        }
      ]
    }
  ]
}

我们找到了解决此问题的方法,方法是使用 Managed identity

二头肌示例:

  • 创建用户分配的标识资源:

     resource UserAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' = {
      name: 'UserAssignedIdentityName'
      ...
     }
    
  • 然后将对象 ID 添加到密钥保管库部署并包括访问权限:

     resource keyVault 'Microsoft.KeyVault/vaults@2020-04-01-preview' = {
      ...
      properties: {
        ...
        accessPolicies: [
          {
            ...
            objectId: UserAssignedIdentity.properties.principalId // Managed Identity
            permissions: {
              ...
            }
          }
        ]
      }
     }
    
  • 例如,部署来自另一个需要访问 Key Vault 的 arm 模板的函数应用程序时。使用现有的用户分配身份:

     resource UserAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' existing = {
      name: 'Name of an existing UAID resource'
     }
    
     resource FunctionApp 'Microsoft.Web/sites@2020-12-01' = {
      ...
      identity:{
        type: 'SystemAssigned, UserAssigned'
        userAssignedIdentities:{
          '${UserAssignedIdentity.id}' :{}
        }
       ...
     }
    

我设法创建了一个解决方案,如果 KeyVault 资源存在,则首先读取 AccessPolicies,然后将它们用作参数。为此,我使用了以下脚本。请注意:您不能将此逻辑合并到一个文件中,否则会出错。

在此示例中,KeyVault 资源的名称是:'MyKeyVault',我的 user-assigned 托管身份是:'MyUserAssignedManagedIdentity'。此身份在 Azure 中 KeyVault 资源的资源组中具有 reader 角色。

这些脚本适用于 KeyVault 不存在和已经存在的情况。当它存在时,它会保留 AccessPolicies。

文件 1:MainDeployment.bicep(您的起点脚本)

var location = resourceGroup().location
var keyVaultName = 'MyKeyVault'

module keyVaultModule 'KeyVaultResourcePreservingAccessPolicies.bicep' = {
  name: 'keyVaultResourcePreservingAccessPolicies_${uniqueString(keyVaultName)}'
  params: {
    location: location
    keyVaultName: keyVaultName
  }
}

文件 2:KeyVaultResourcePreservingAccessPolicies.bicep:

param location string
param keyVaultName string

module resourceExistsModule 'ResourceExists.bicep' = {
  name: 'resourceExists_${uniqueString(keyVaultName)}'
  params: {
    location: location
    resourceName: keyVaultName
    // User assigned managed identity that is used to execute the deployment script on the resource group
    // User assigned Managed Identity info: https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview
    // User assigned Managed Identity needs reader role on the resource group of the resource (See Access Policies of the resource group)
    identityPrincipalId: 'MyUserAssignedManagedIdentity'
  }
}

module keyVaultModule 'KeyVaultResourceUsingExistingAccessPolicies.bicep' = {
  name: 'keyVaultResourceUsingExistingAccessPolicies_${uniqueString(keyVaultName)}'
  params: {
    location: location
    keyVaultName: keyVaultName
    keyVaultResourceExists: resourceExistsModule.outputs.exists
  }
}

文件 3:KeyVaultResourceUsingExistingAccessPolicies.bicep

param location string

param keyVaultName string
param keyVaultResourceExists bool

resource existingKeyVaultResource 'Microsoft.KeyVault/vaults@2021-11-01-preview' existing = if (keyVaultResourceExists) {
  name: keyVaultName
}

module keyVaultModule 'KeyVaultResource.bicep' = {
  name: 'KeyVaultResource_${uniqueString(keyVaultName)}'
  params: {
    location: location
    keyVaultName: keyVaultName
    accessPolicies: keyVaultResourceExists ? existingKeyVaultResource.properties.accessPolicies : []
  }
  dependsOn: [
    existingKeyVaultResource
  ]
}

文件 4:ResourceExists.bicep:

// Based on https://github.com/olafloogman/BicepModules/blob/main/resource-exists.bicep and https://ochzhen.com/blog/check-if-resource-exists-azure-bicep

@description('Resource name to check in current scope (resource group)')
param resourceName string

@description('Resource ID of user managed identity with reader permissions in current scope')
param identityPrincipalId string

param location string
param utcValue string = utcNow()

var userAssignedIdentity = resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.ManagedIdentity/userAssignedIdentities', '${identityPrincipalId}')

// The script below performs an 'az resource list' command to determine whether a resource exists
resource resource_exists_script 'Microsoft.Resources/deploymentScripts@2020-10-01' = {
  name: 'resourceExistsDeploymentScript_${resourceName}'
  location: location
  kind: 'AzureCLI'
  identity: {
    type: 'UserAssigned'
    userAssignedIdentities: {
      '${userAssignedIdentity}': {}
    }
  }
  properties: {
    forceUpdateTag: utcValue
    azCliVersion: '2.15.0'
    timeout: 'PT10M'    
    scriptContent: 'result=$(az resource list --resource-group ${resourceGroup().name} --name ${resourceName}); echo $result; echo $result | jq -c \'{Result: map({name: .name})}\' > $AZ_SCRIPTS_OUTPUT_PATH;'
    cleanupPreference: 'OnSuccess'
    retentionInterval: 'P1D'
  }
}

//Script returns something like: //[{"name":"MyKeyVault"}]
output exists bool = length(resource_exists_script.properties.outputs.Result) > 0

文件 5:KeyVaultResource.bicep

param location string
param keyVaultName string
param accessPolicies array

resource keyVaultResource 'Microsoft.KeyVault/vaults@2021-11-01-preview' = {
  name: keyVaultName
  location: location
  properties: {
    sku: {
      family: 'A'
      name: 'standard'
    }
    accessPolicies: accessPolicies
    tenantId: subscription().tenantId
    enableSoftDelete: true
    softDeleteRetentionInDays: 90
    enableRbacAuthorization: false
    enablePurgeProtection: true
    publicNetworkAccess: 'Enabled'
  }
}