azureazure-resource-managerazure-keyvaultazure-bicepazure-private-dns

Private KeyVault secret reference from Function Apps in multiple VNets in the same resource group


I have setup a single KeyVault and 2 Function Apps using Bicep. To test I build the bicep and input the ARM template into "Deploy custom template" on Azure portal. KeyVault deployed with public network access disabled, a private endpoint, and both are linked:

...
var is_private_access = SUBNET_ID != ''

resource KeyVault 'Microsoft.KeyVault/vaults@2024-04-01-preview' = {
  name: key_vault_unique_name
  location: REGION
  properties: {
    accessPolicies: ENABLE_RBAC_AUTHORIZATION ? [] : ACCESS_POLICIES
    createMode: CREATE_MODE
    enabledForDeployment: ENABLED_FOR_DEPLOYMENT
    enabledForDiskEncryption: ENABLED_FOR_DISK_ENCRYPTION
    enabledForTemplateDeployment: ENABLED_FOR_TEMPLATE_DEPLOYMENT
    enablePurgeProtection: ENABLE_PURGE_PROTECTION
    enableRbacAuthorization: ENABLE_RBAC_AUTHORIZATION
    enableSoftDelete: ENABLE_SOFT_DELETE
    networkAcls: {}
    publicNetworkAccess: is_private_access ? 'disabled' : 'enabled'
    sku: {
      family: 'A'
      name: 'Standard'
    }
    softDeleteRetentionInDays: SOFT_DELETE_RETENTION_DAYS
    tenantId: TENANT_ID
    vaultUri: VAULT_URI
  }
}

resource PrivateEndpoint 'Microsoft.Network/privateEndpoints@2021-02-01' = if (is_private_access) {
  name: take('VaultPrivateEndpoint-${KEY_VAULT_UNIQUE_STRING}', 64)
  location: REGION
  properties: {
    subnet: {
      id: SUBNET_ID
    }
    privateLinkServiceConnections: [
      {
        name: '${KeyVault.name}-file-private-link-connection'
        properties: {
          privateLinkServiceId: KeyVault.id
          groupIds: [
            'vault'
          ]
        }
      }
    ]
  }
}

resource VaultPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' existing = if (is_private_access) {
  name: 'privatelink.vaultcore.azure.net'
}

resource PrivateDnsZoneLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = if (is_private_access) {
  parent: VaultPrivateDnsZone
  name: take('virtual-network-link-${KEY_VAULT_UNIQUE_STRING}', 64)
  location: REGION
  properties: {
    registrationEnabled: false
    virtualNetwork: {
      id: VNET_ID
    }
  }
}

resource PrivateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-11-01' = if (is_private_access) {
  parent: PrivateEndpoint
  name: 'VaultDnsGroup'
  properties: {
    privateDnsZoneConfigs: [
      {
        name: 'vaultConfig'
        properties: {
          privateDnsZoneId: VaultPrivateDnsZone.id
        }
      }
    ]
  }
}

Now the Function Apps get created one at a time, each in its own VNet and subnet. Private endpoints and links are created for each subnet to the Private DNS 'privatelink.vaultcore.azure.net'. First link goes smoothly, references to secrets can be pulled to fill environment variables of that Function App. Once the second private link deploys neither can connect.

After a ton of troubleshooting I found what happens exactly at that moment. Second link registers an A record in the private DNS zone with the same subdomain (.privatelink.vaultcore.azure.net) and overrides the IP address. There seems to be no method to deploy 2 private endpoints from different VNets for the same KeyVault.

Manually I managed to find a workaround, overriding the A record to have multiple IPs. This method did not work as part of the ARM template deployment so far since the IPs are generated during deployment and I cannot reference them from the private endpoint resource - else I get an error saying the deployer can't read the endpoint's properties that were not set at initialization.

Additional context: I followed this KeyVault private deployment guide, and found a troubleshooting guide for some similar scenarios. But found the information misleading since they suggest a private DNS zone per VNet when you can't create multiple of those in a single resource group with the same name, which has to be "privatelink.vaultcore.azure.net".


Solution

  • I ended up using a powershell script that runs in place of the resource creating the DNS zone group. It saves the current A record IP addresses, creates the group himself, then appends the IP addresses back into the DNS record. That was the only way I found of making it work in my situation.

    Bicep resource used: Microsoft.Resources/deploymentScripts@2020-10-01

    And here is the script:

    param(
      [string] $deploymentResourceGroupName,
      [string] $keyVaultName,
      [string] $privateEndpointName,
      [string] $dnsGroupName
    )
    
    # 1
    try {
      $originalDnsZoneRecord = Get-AzPrivateDnsRecordSet -ResourceGroupName $deploymentResourceGroupName -ZoneName "privatelink.vaultcore.azure.net" -Name $keyVaultName -RecordType A -ErrorAction SilentlyContinue
      $originalDnsRecordIpAddresses = if ($originalDnsZoneRecord) {
          $originalDnsZoneRecord.Records.Ipv4Address
      }
    }
    catch {
      $originalDnsRecordIpAddresses = $()
    }
    
    # 2
    $zone = Get-AzPrivateDnsZone -ResourceGroupName $deploymentResourceGroupName -Name "privatelink.vaultcore.azure.net"
    $dnsZoneGroupConfig = New-AzPrivateDnsZoneConfig -Name $dnsGroupName -PrivateDnsZoneId $zone.ResourceId
    $originalDnsZoneGroup = Get-AzPrivateDnsZoneGroup -ResourceGroupName $deploymentResourceGroupName -PrivateEndpointName $privateEndpointName
    $dnsZoneGroupName = ""
    if ($originalDnsZoneGroup.Count -eq 0) {
      $dnsZoneGroupName = "vaultDnsZoneGroup"
      Set-AzPrivateDnsZoneGroup -ResourceGroupName $deploymentResourceGroupName -PrivateEndpointName $privateEndpointName -name $dnsZoneGroupName -PrivateDnsZoneConfig $dnsZoneGroupConfig
    } else {
      $dnsZoneGroupName = $originalDnsZoneGroup.Name
    }
    
    # 3
    $newDnsZoneGroup = Get-AzPrivateDnsZoneGroup -ResourceGroupName $deploymentResourceGroupName -PrivateEndpointName $privateEndpointName -name $dnsZoneGroupName
    $newIp = $newDnsZoneGroup.PrivateDnsZoneConfigs.RecordSets.IpAddresses
    
    $currentDnsZoneRecord = Get-AzPrivateDnsRecordSet -ResourceGroupName $deploymentResourceGroupName -ZoneName "privatelink.vaultcore.azure.net" -Name $keyVaultName -RecordType A
    $currentDnsRecordIpAddresses = $currentDnsZoneRecord.Records.Ipv4Address
    
    # 4
    $allIpAddresses = $($originalDnsRecordIpAddresses; $newIp)
    foreach ($ip in $allIpAddresses) {
      if ($currentDnsRecordIpAddresses -NotContains $ip) {
        Add-AzPrivateDnsRecordConfig -RecordSet $currentDnsZoneRecord -Ipv4Address $ip
      }
    }
    Set-AzPrivateDnsRecordSet -RecordSet $currentDnsZoneRecord