Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Request: DINE policy for private endpoint -> private DNS zone linking with static webapps #1223

Open
rybal06 opened this issue Oct 18, 2023 · 7 comments

Comments

@rybal06
Copy link

rybal06 commented Oct 18, 2023

Details of the scenario you tried and the problem that is occurring

We are utilizing this Azure "best practice" architecture at scale with dozens of resource types without any issues.

https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/private-link-and-dns-integration-at-scale

This works great for every resource type we've encountered, except for static websites. To use Azure webapps as an example:

  1. Platform user deploys Azure private endpoint which connects to a WebApp; and leaves private dns zone configuration empty.
  2. Azure policy finds this configuration using DINE policy and adds the private DNS zone configuration to a central private DNS zone for the resource type.

Why?
Platform users don't need to be concerned with DNS records, DNS servers, or DNS management. They'd don't require any permissions on DNS zones which are shared by many application workloads.

The problem with static sites

The problem is making this work with static websites. Upon creation, static webapps put a DNS Zone partition ID in the domain name, i.e. white-flower-048d2aa10.privatelink.3.azurestaticapps.net

This means that the domain names for static web apps vary, for example:
1.azurestaticapps.net
2.azurestaticapps.net
3.azurestaticapps.net
4.azurestaticapps.net
5.azurestaticapps.net
6.azurestaticapps.net
...

The way the other DINE policies work is by matching upon these properties, for example:

                "allOf": [
                  {
                    "field": "Microsoft.Network/privateEndpoints/privateLinkServiceConnections[*].privateLinkServiceId",
                    "contains": "Microsoft.Web/sites"
                  },
                  {
                    "field": "Microsoft.Network/privateEndpoints/privateLinkServiceConnections[*].groupIds[*]",
                    "equals": "sites"
                  }

ref: https://github.com/Azure/azure-policy/blob/master/built-in-policies/policyDefinitions/App%20Service/AppService_PrivateZoneGroup_DINE.json

Azure policy then configures the private DNS zone on the private endpoint, which creates the DNS record.

The problem? Regardless of the private dns zone partition (i.e. 1.azurestaticapps.net vs 2.azurestaticapps.net); static webapps all use the same private link service connection group ID so it is not possible to map the group ID to a private dns zone correctly.

Nowhere on the private endpoint resource is the private dns zone ID partition represented. The only way I have found to find that is to look at the static website linked to the private endpoint; parse the "Default Domain Name field" and extract the zone name from the uri, i.e. 1.azurestaticapps.net.

Suggested solution to the issue

Workaround

The workaround I am planning to test is asking platform users to populate a tag on private endpoints for Azure static webapps which contains the private dns zone name, i.e. StaticSitesDomainName: 9.azurestaticapps.net. We can have a custom policy then parse that value to determine which private dns zone to link the private endpoint into.

This isn't ideal as it is a one-off solution for a single Azure resource type and isn't our typical usage for tags.

Better Solution

It would be great to see the Azure policy team collaborate with the Azure static websites (and potentially DNS teams) to come up with a more permanent solution.

All Azure resource types we've encountered work perfectly with the recommended architecture https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/private-link-and-dns-integration-at-scale except for Static Sites which introduces challenges for our platform users.

A few ideas here:

  • Confirm my understanding that there is not any existing capability within Azure Policy to use a lookup function to find the private dns domain name for static sites.
  • Verify what I have reported and ensure there is not an existing solution which is more elegant or user-friendly than the workaround I suggested.
  • Work with the static sites team to see understand why they made this design decision and to see if there is something they could change in their platform to accomodate using a single domain name for private endpoints as other Azure services do.
  • See if it is possible to introduce some type of lookup feature in Azure policy, so that from the private endpoint resource policy can determine the correct private dns zone for static sites.
  • See if the static sites team can create different private endpoint group IDs for each of the domains they use, for example staticSites1 = 1.azurestaticapps.net.
@rybal06
Copy link
Author

rybal06 commented Oct 18, 2023

The workaround of also adding the domain name as a custom tag, and leveraging it in the Azure policy does work as expected. It is not super intuitive to our platform users and is different than every other Azure resource type, but it is working.

@matsest
Copy link

matsest commented Oct 30, 2023

Confirm my understanding that there is not any existing capability within Azure Policy to use a lookup function to find the private dns domain name for static sites.

@rybal06 It's not a capability of Azure Policy, but rather of ARM templates :) I have made a workaround for this. Since the resource Id's of the private link zones are deterministic we can use the resource group Id that holds the private DNS zones as an input, together with a template function that does a lookup on the private endpoint resource to get the hostname together with a fair amount of string manipulation. It's not pretty, but it removes the need for having this as a separate tag on the resource.

To simplify authoring I made the template part of the DINE policy as Bicep, and build it and paste it into the template part of the policy definition.

Expand for script samples
param privateDnsZoneResourceGroupId string
param privateEndpointName string

resource pe 'Microsoft.Network/privateEndpoints@2022-07-01' existing = {
  name: privateEndpointName
}

// <host>.2.azurestaticapps.net or <host>.azurestaticapps.net
var fqdn = pe.properties.customDnsConfigs[0].fqdn
// [ '<host>', '2', 'azurestaticapps', 'net']
var parts = split(fqdn, '.')
// '2' or 'azurestaticapps'
var prefix = parts[1]
// resolve name of private DNS Zone based on the prefix
var privateDnsZone = !(prefix == 'azurestaticapps') ? 'privatelink.${prefix}.azurestaticapps.net' : 'privatelink.azurestaticapps.net'

resource peDnsConfig 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2022-07-01' = {
  name: 'deployedByPolicy'
  parent: pe
  properties: {
    privateDnsZoneConfigs: [
      {
        name: 'staticSites-privateDnsZone'
        properties: {
          privateDnsZoneId: '${privateDnsZoneResourceGroupId}/providers/Microsoft.Network/privateDnsZones/${privateDnsZone}'
        }
      }
    ]
  }
}

output privateDnsZoneId string = '${privateDnsZoneResourceGroupId}/providers/Microsoft.Network/privateDnsZones/${privateDnsZone}'

The policyRule for this would then be:

{
  "if": {
    "allOf": [
      {
        "field": "type",
        "equals": "Microsoft.Network/privateEndpoints"
      },
      {
        "count": {
          "field": "Microsoft.Network/privateEndpoints/privateLinkServiceConnections[*]",
          "where": {
            "allOf": [
              {
                "field": "Microsoft.Network/privateEndpoints/privateLinkServiceConnections[*].privateLinkServiceId",
                "contains": "Microsoft.Web/staticSites"
              },
              {
                "field": "Microsoft.Network/privateEndpoints/privateLinkServiceConnections[*].groupIds[*]",
                "equals": "staticSites"
              }
            ]
          }
        },
        "greaterOrEquals": 1
      }
    ]
  },
  "then": {
    "effect": "[parameters('effect')]",
    "details": {
      "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups",
      "roleDefinitionIds": [
        "/providers/Microsoft.Authorization/roleDefinitions/4d97b98b-1d4f-4787-a291-c67834d212e7"
      ],
      "existenceCondition": {
        "field": "name",
        "equals": "deployedByPolicy"
      },
      "deployment": {
        "properties": {
          "mode": "Incremental",
          "template": {
            "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
            "contentVersion": "1.0.0.0",
            "parameters": {
              "privateDnsZoneResourceGroupId": {
                "type": "string"
              },
              "privateEndpointName": {
                "type": "string"
              },
              "location": {
                "type": "string"
              }
            },
            "resources": [
              {
                "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups",
                "apiVersion": "2022-07-01",
                "location": "[parameters('location')]",
                "name": "[format('{0}/{1}', parameters('privateEndpointName'), 'deployedByPolicy')]",
                "properties": {
                  "privateDnsZoneConfigs": [
                    {
                      "name": "staticSites-privateDnsZone",
                      "properties": {
                        "privateDnsZoneId": "[format('{0}/providers/Microsoft.Network/privateDnsZones/{1}', parameters('privateDnsZoneResourceGroupId'), if(not(equals(split(reference(resourceId('Microsoft.Network/privateEndpoints', parameters('privateEndpointName')), '2022-07-01').customDnsConfigs[0].fqdn, '.')[1], 'azurestaticapps')), format('privatelink.{0}.azurestaticapps.net', split(reference(resourceId('Microsoft.Network/privateEndpoints', parameters('privateEndpointName')), '2022-07-01').customDnsConfigs[0].fqdn, '.')[1]), 'privatelink.azurestaticapps.net'))]"
                      }
                    }
                  ]
                }
              }
            ]
          },
          "parameters": {
            "privateDnsZoneResourceGroupId": {
              "value": "[parameters('privateDnsZoneResourceGroupId')]"
            },
            "privateEndpointName": {
              "value": "[field('name')]"
            },
            "location": {
              "value": "[field('location')]"
            }
          }
        }
      }
    }
  }
}

It's not pretty, but it removes this special consideration from a user perspective and brings the complexity into the ones who manages policies instead.

Only issue I'm facing now is that I'm not sure how many possible partition numbers there is to create private DNS zones for..

@rybal06
Copy link
Author

rybal06 commented Oct 31, 2023

@matsest Thank you so much for sharing! I likely won't get back to this in the next few weeks, but it seems like a much more elegant solution than my workaround. I had worked a support case with the Azure policy team previously and they didn't believe it was possible to do this type of lookup.

I also asked the collab support engineer from the Static App Team about the number of DNS zone partitions. They said that today Microsoft is using 0-3, but may add more in the future. I pre-populated 0-9 myself. This might be outdated as this was six months ago, but I hope it helps.

It would be great if the team maintaining this repo could merge your approach into a supported policy.

@rybal06
Copy link
Author

rybal06 commented Nov 6, 2023

I did a some more research, what is interesting is that this policy alias doesn't seem to work, otherwise this would be so much simpler!

Microsoft.Network/privateEndpoints/customDnsConfigs[*].fqdn

The value is in the policy alias, but it doesn't seem to work. I've tried:

  • Matching upon it using both a wildcard with like *
  • Matching using equals and the exact value
  • Adding a 1 minute evaluation delay (theory was that maybe the value isn't populated immediately upon provisioning?)

If the Alias worked, we wouldn't need to do the lookup using the ARM/bicep template function at all as the information we are grabbing is already present.

A quick internet search on Microsoft.Network/privateEndpoints/customDnsConfigs[*].fqdn doesn't turn up many results; however at least one other person has tried using it and failed: https://stackoverflow.com/questions/75889272/azure-custom-deployifnotexists-policy-logic

Does anyone have any insights as to why this Alias doesn't work in policy evaluation? Is this a bug in the alias?

@MJ-Coder
Copy link

MJ-Coder commented Nov 9, 2023

We encounter the exact same bug as @rybal06 mentioned here
We try to use the policy alias "Microsoft.Network/privateEndpoints/customDnsConfigs[*].fqdn" also for evaluating if an Azure App Service app is hosted by an App Service Environment or not, by looking for the domain suffix appserviceenvironment.net

Microsoft please fix this bug!

I did a some more research, what is interesting is that this policy alias doesn't seem to work, otherwise this would be so much simpler!

Microsoft.Network/privateEndpoints/customDnsConfigs[*].fqdn

The value is in the policy alias, but it doesn't seem to work. I've tried:

  • Matching upon it using both a wildcard with like *
  • Matching using equals and the exact value
  • Adding a 1 minute evaluation delay (theory was that maybe the value isn't populated immediately upon provisioning?)

If the Alias worked, we wouldn't need to do the lookup using the ARM/bicep template function at all as the information we are grabbing is already present.

A quick internet search on Microsoft.Network/privateEndpoints/customDnsConfigs[*].fqdn doesn't turn up many results; however at least one other person has tried using it and failed: https://stackoverflow.com/questions/75889272/azure-custom-deployifnotexists-policy-logic

Does anyone have any insights as to why this Alias doesn't work in policy evaluation? Is this a bug in the alias?

@MJ-Coder
Copy link

I've reported this as a bug in a service request, I hope they will pass it to their policy product team(s).

@oc159
Copy link

oc159 commented May 2, 2024

We're also experiencing this issue with the policy alias
Microsoft.Network/privateEndpoints/customDnsConfigs[*].fqdn
I'm keen to limit the application of the policy if the IP of the PE does not fit within our vWan CIDR Block.
Seems cumbersome but i'm looking to avoid issues down the road.

Hopefully MS has provided an update?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants