Skip to content

Secure Networking · Runbook

Private Endpoints DNS baseline for Azure

• • 7 min read

A practical baseline for Azure Private Endpoints and DNS: ownership, zone design, resolver routing, and onboarding patterns that prevent midnight outages.

  • Effort: 1–2 days
  • Risk: Medium–High
  • Rollback: Plan required

Applies to: Azure Private Link · Private Endpoints · Private DNS Zones · Azure DNS Private Resolver · Hub/Spoke

On this page
  1. Why Private Endpoints break at 02:00
  2. What good looks like
  3. Baseline decision points
  4. Signal vs noise
  5. Phased rollout
  6. Runbook: Private Endpoints + DNS baseline
  7. Policy guardrails
  8. Validation checks
  9. Common pitfalls
  10. Rollback / back-out plan
TL;DR
  • Put every Private DNS zone in one platform subscription and guard it like any other shared service.
  • Use Azure DNS Private Resolver + hub-only links: zones link to the hub vNet only, and spokes resolve through the resolver’s inbound endpoint. Vending wires spoke DNS, not zone links.
  • Day 0: deploy zones/resolver, link zones to the hub, wire spoke DNS through vending, publish the request workflow. Day 30: add policy/monitoring, integrate with ops. Quarterly: audit links vs inventory and renew rollbacks.
  • Terraform/Bicep only. Portal deployments create outages at 02:00.
  • KQL + Policy catch drift; playbooks and rollbacks exist per service.
Before you start
  • Access: Private DNS Zone Contributor on the platform DNS resource group, Network Contributor on the hub vNet for the zone links, and on spoke vNets to set their DNS servers.
  • Tooling: Azure CLI 2.60+, Terraform 1.7+ with azurerm 5.x. In 5.0, azurerm_private_dns_zone_virtual_network_link dropped private_dns_zone_name and resource_group_name in favour of private_dns_zone_id; the example below uses the 5.x shape.
  • Know your TTLs: DNS changes are not instant and rollback is not instant either. Record the TTLs on the zones you’re touching before you start.
  • Blast radius: removing a zone’s hub link breaks resolution of that zone for every spoke that resolves through the hub, usually silently.

Why Private Endpoints break at 02:00

Private Endpoints without governance are outage factories. The moment someone links a zone to the wrong vNet, deletes the record, or forgets reverse lookups, apps stop resolving. A baseline keeps ownership, automation, and rollback in the platform team instead of relying on every workload engineer remembering the DNS handshake.

What good looks like

  • Zones in one place – all Azure Private DNS zones owned by platform, tagged, versioned.
  • Hub-based resolver – Azure DNS Private Resolver (inbound/outbound) deployed to the hub vNet, which is the only vNet linked to the zones. Spokes resolve through its inbound endpoint.
  • Automated DNS wiring – subscription vending points each spoke at the resolver; Private Endpoint DNS zone groups register the records.
  • Policy guardrails – Azure Policy ensures Private Endpoints only land in approved subnets and register in the central zones.
  • Monitoring + rollback – scripts compare expected vs actual links, alerts fire on deletion, playbooks exist per service.

Baseline decision points

  1. Zone ownership – Platform subscription is the default; avoids sprawl and simplifies approvals.
  2. Linking pattern – Hub-only links + resolver vs direct workload links. Hub pattern reduces churn and RBAC bloat.
  3. Per-service vs shared zones – Usually one zone per Azure service (privatelink.<service>…, for example privatelink.blob.core.windows.net); easier for lifecycle and rollback.
  4. Automation tooling – Terraform/Bicep modules invoked by vending or change pipeline. No portal.
  5. Resolver strategy – Azure DNS Private Resolver with inbound/outbound endpoints in hub VNets; no custom DNS VMs.
  6. Subscription vending hook – Decide where spoke DNS is set (custom DNS servers or a ruleset link) and make sure it runs in the networking module, before the first Private Endpoint request.
  7. Monitoring + drift – How you detect missing links, stale records, or unauthorised zones. Use KQL and automation.

Signal vs noise

  • Enable now: Zones, resolver, hub links, spoke DNS automation, policies for subnet restrictions, monitoring scripts, request workflow.
  • Enable at Day 30: Reverse lookup zones and advanced alerting once the baseline is stable.
  • Probably never: Letting workloads own their own zones, manual linking (“just this once”), or building custom DNS servers because “it feels familiar.”

Phased rollout

  • Day 0 baseline – Deploy zones/resolver, build the Terraform module that links zones to the hub and sets spoke DNS, document the request workflow, integrate with vending.
  • Day 30 hardening – Enable policy guardrails, add reverse lookup zones, wire drift detection into alerting, start logging.
  • Quarterly review – Compare expected vs actual links, review resolver ruleset links and rules, test rollback playbooks, update service catalogue.

Runbook: Private Endpoints + DNS baseline

  1. Define ownership and landing zones for DNS resources

    Put private DNS zones, the Azure DNS Private Resolver, and automation identities in a platform subscription. Document the RBAC split: platform controls records; workloads request via automation or service management.

  2. Catalogue the services you allow via Private Link

    List the Azure services (e.g. Key Vault, Storage, SQL, Container Registry) that are permitted. This drives which private zones you deploy. Keep the list under version control so policy and automation stay aligned.

  3. Deploy standard private DNS zones with IaC

    Create the per-service zones ahead of time. Use consistent naming and tag them with owner + lifecycle information. This Terraform snippet keeps the catalogue consistent.

    HCL
    variable "zones" {
      type    = list(string)
      default = [
        "privatelink.vaultcore.azure.net",
        "privatelink.blob.core.windows.net",
        "privatelink.database.windows.net"
      ]
    }
    
    resource "azurerm_private_dns_zone" "this" {
      for_each = toset(var.zones)
    
      name                = each.value
      resource_group_name = "rg-platform-dns"
      tags = {
        owner   = "platform-dns"
        purpose = "PrivateLink"
      }
    }
  4. Provision Azure DNS Private Resolver in the hub

    Deploy inbound/outbound endpoints in the hub vNet and link every private zone to the hub vNet only. Spokes then reach the zones one of two ways:

    • Custom DNS (centralised): set the spoke vNet’s DNS servers to the inbound endpoint IP.
    • Ruleset link (distributed): link a DNS forwarding ruleset to the spoke, with a rule per privatelink.* domain that forwards to the inbound endpoint IP.

    Forwarding rules send a domain to one or more DNS server IPs; they don’t point at a zone. Use them on the outbound endpoint for namespaces that live elsewhere, such as on-premises domains. Never link a ruleset that forwards to the inbound endpoint to the hub vNet itself, or you get a resolution loop. This keeps spoke networks lightweight and avoids custom DNS servers.

  5. Automate vNet linking and zone registration

    Link each zone to the hub vNet once, in the same module that creates it. When a subscription onboards, the vending pipeline sets the spoke’s DNS (inbound endpoint IP or ruleset link). Don’t link zones to spokes: it brings back the churn and RBAC sprawl the hub model avoids. Avoid manual changes; they inevitably drift. Store metadata so you can report which spokes use which DNS path.

    HCL
    variable "hub_vnet_id" {
      type = string
    }
    
    # azurerm 5.x: the link takes the zone ID; there is no zone name or
    # resource group argument any more.
    resource "azurerm_private_dns_zone_virtual_network_link" "hub" {
      for_each = azurerm_private_dns_zone.this
    
      name                 = "link-hub"
      private_dns_zone_id  = each.value.id
      virtual_network_id   = var.hub_vnet_id
      registration_enabled = false
    }
  6. Create a Private Endpoint request workflow

    Provide a template (Terraform module, ARM/Bicep snippet, or portal checklist) that workloads must follow. Include parameters for approval, target subnet, the DNS zone group, and rollback. Make the workflow part of your change process.

  7. Enforce policy guardrails

    Use Azure Policy to prevent random private endpoints in forbidden subnets, enforce Private DNS zone registration, and audit any endpoint without an RBAC-approved owner. This keeps requests visible.

  8. Add monitoring and drift detection

    Run scheduled scripts (Logic App/Azure Automation) to compare declared zone links against actual ones and check spoke DNS settings. Alert when zones are unlinked, a spoke stops pointing at the resolver, or records are missing. Combine with activity log alerts for zone deletions.

  9. Feed the pattern into subscription vending

    When a new subscription is created, ensure the standard networking template sets the spoke DNS servers to the resolver inbound IP (or links the forwarding ruleset). Workloads should inherit the ability to resolve private endpoints without extra steps.

Policy guardrails

Use Bicep to prevent rogue Private Endpoints and make sure each endpoint registers in the central zones. Replace policy IDs with your own definitions.

BICEP
targetScope = 'managementGroup'

@description('Region for the assignment identity. Not where policy applies.')
param location string = 'uksouth'

// Resolve real definition IDs first, e.g.:
//   az policy definition list --query "[?contains(displayName,'<search term>')].{name:displayName,id:id}" -o table
resource initiative 'Microsoft.Authorization/policySetDefinitions@2023-04-01' = {
  name: 'private-link-guardrails'
  properties: {
    displayName: 'Private Link Guardrails'
    description: 'Keep private endpoints in approved subnets and register them in the central zones.'
    policyType: 'Custom'
    policyDefinitions: [
      {
        policyDefinitionReferenceId: 'allowed-subnets'
        policyDefinitionId: '/providers/Microsoft.Authorization/policyDefinitions/<allowed-subnets-id>'
      }
      {
        policyDefinitionReferenceId: 'deploy-pe-dns-zone-group'
        policyDefinitionId: '/providers/Microsoft.Authorization/policyDefinitions/<deploy-pe-dns-zone-group-id>'
      }
    ]
  }
}

resource assignment 'Microsoft.Authorization/policyAssignments@2023-04-01' = {
  name: 'private-link-guardrails'
  // Required: the initiative contains a deployIfNotExists effect, which cannot
  // remediate without an identity that holds rights at the assigned scope.
  location: location
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    displayName: 'Private Link Guardrails'
    policyDefinitionId: initiative.id
  }
}

resource remediationRights 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: guid(managementGroup().id, assignment.id, 'Contributor')
  properties: {
    // Contributor — narrow this to the least-privileged role your definitions need.
    roleDefinitionId: tenantResourceId(
      'Microsoft.Authorization/roleDefinitions',
      'b24988ac-6180-42a0-ab88-20f7382dd24c'
    )
    principalId: assignment.identity.principalId
    principalType: 'ServicePrincipal'
  }
}

Validation checks

Drift detection here is a state question, not a log question: a zone that was never linked to a vNet generates no events at all, so no amount of log querying will surface it. Use Azure Resource Graph to read the actual links, then diff against what your IaC declares. In the hub model every link should point at the hub vNet, so uncomment the last filter to list only the drift.

KUSTO
resources
| where type =~ 'microsoft.network/privatednszones/virtualnetworklinks'
| project zone = tostring(split(id, '/')[8]),
          link = name,
          vnet = tostring(properties.virtualNetwork.id),
          registrationEnabled = tobool(properties.registrationEnabled),
          subscriptionId
| order by zone asc, vnet asc
// | where vnet !~ '<hub-vnet-resource-id>'

To spot the opposite problem, zones with no links at all (usually a hub link that was removed or never created):

KUSTO
resources
| where type =~ 'microsoft.network/privatednszones'
| extend linkCount = toint(properties.numberOfVirtualNetworkLinks)
| where linkCount == 0
| project subscriptionId, resourceGroup, zone = name

Then check the spokes. This lists vNets that don’t use the resolver inbound IP as a DNS server. Expect the hub itself, and any spokes on the ruleset-link pattern, to appear; everything else is drift:

KUSTO
resources
| where type =~ 'microsoft.network/virtualnetworks'
| extend dnsServers = properties.dhcpOptions.dnsServers
| where isnull(dnsServers) or array_length(dnsServers) == 0
     or tostring(dnsServers) !contains '<resolver-inbound-ip>'
| project subscriptionId, resourceGroup, vnet = name,
          dnsServers = tostring(dnsServers)

Logs still earn their place for the destructive events. This alerts on links being removed or zones deleted, which is the failure mode that pages you at 02:00:

KQL
AzureActivity
| where TimeGenerated > ago(1d)
| where OperationNameValue has "PRIVATEDNSZONES"
| where OperationNameValue has "DELETE"
| where ActivityStatusValue =~ "Success"
| project TimeGenerated, Caller, OperationNameValue, _ResourceId
| order by TimeGenerated desc
  • All approved private DNS zones exist in the platform subscription with correct tags.

  • Azure DNS Private Resolver endpoints are deployed, healthy, and documented.

  • Every private zone is linked to the hub vNet only, and every spoke resolves through the resolver inbound endpoint, all via automation (no manual leftovers).

  • Private Endpoint requests use the standard workflow and land in approved subnets.

  • Azure Policy assignments audit or deny unsupported Private Link usage.

  • Monitoring alerts fire if a zone link is removed or a zone is deleted.

  • Subscription vending pipelines automatically grant workloads DNS resolution from day zero.

  • Rollback procedures for each service (Key Vault, Storage, SQL, etc.) are written and tested.

Common pitfalls

Pitfall: portal-only deployment

If each team creates zones and endpoints in the portal, you will never regain control. Use IaC or service catalogue items.

Pitfall: mixed ownership

Putting some zones in workload subs and others in platform subs is a support nightmare. Pick one model and document it.

Pitfall: no reverse lookup or resolver routing

Logs and diagnostics often need reverse DNS. Ignoring it makes investigations painful.

Pitfall: hub DNS not ready

If the resolver isn’t in place when workloads arrive, they bake in custom DNS workarounds that are hard to remove later.

Pitfall: no monitoring

Zone deletions and link removals go unnoticed until production fails. Monitor and alert on drift.

Rollback / back-out plan

  • To remove a Private Endpoint: delete the endpoint, clear the DNS A record, and run the automation that restores public connectivity if needed. Expect short outages.
  • To revert resolver changes: disable forwarding rules, but note that cached results may linger until TTLs expire. Communicate clearly with workloads before flipping.
  • To undo zone links: in the hub model a zone has one link, so removing it affects every spoke at once. Remove the link and flush DNS on affected hosts. Document why the link was removed and ensure alternatives (public endpoints or other regions) exist.

Rollback is never “instant” because DNS caching and dependency on Private Link may require maintenance windows. Keep playbooks per service.