Every Azure PaaS service ships with a public endpoint by default. Your storage account, your SQL database, your Key Vault, all reachable from the internet with nothing but a firewall rule between your data and the world. For most enterprises that is unacceptable, and the answer is Private Link. It sounds simple: give the PaaS service a private IP inside your VNet. In practice, the networking part takes five minutes and the DNS part is where every failed implementation I have debugged went wrong.
How Private Link Works
A private endpoint is a network interface in your subnet that maps to a specific PaaS resource. Traffic to it stays on the Microsoft backbone, never touches the public internet, and works across peered VNets, VPN, and ExpressRoute. Once the endpoint exists, you disable public network access on the resource and the only way in is through your network.
resource "azurerm_storage_account" "data" {
name = "stcorpdataprod"
resource_group_name = azurerm_resource_group.app.name
location = "westeurope"
account_tier = "Standard"
account_replication_type = "ZRS"
public_network_access_enabled = false
}
resource "azurerm_private_endpoint" "blob" {
name = "pep-stcorpdataprod-blob"
location = "westeurope"
resource_group_name = azurerm_resource_group.app.name
subnet_id = azurerm_subnet.endpoints.id
private_service_connection {
name = "psc-blob"
private_connection_resource_id = azurerm_storage_account.data.id
subresource_names = ["blob"]
is_manual_connection = false
}
private_dns_zone_group {
name = "dns"
private_dns_zone_ids = [azurerm_private_dns_zone.blob.id]
}
}
Note the subresource name. Storage alone has blob, file, queue, table, web, and dfs as separate subresources, and each needs its own endpoint if you use it privately.
The DNS Architecture That Actually Works
Here is the part everyone gets wrong. The client must still resolve the original FQDN, for example stcorpdataprod.blob.core.windows.net. Public DNS returns a CNAME to privatelink.blob.core.windows.net, and your private DNS zone for that name returns the private IP. If the client cannot resolve the privatelink zone, it silently falls back to the public IP and either fails or, worse, works over the public path without anyone noticing.
resource "azurerm_private_dns_zone" "blob" {
name = "privatelink.blob.core.windows.net"
resource_group_name = azurerm_resource_group.dns.name
}
resource "azurerm_private_dns_zone_virtual_network_link" "blob_hub" {
name = "link-hub"
resource_group_name = azurerm_resource_group.dns.name
private_dns_zone_name = azurerm_private_dns_zone.blob.name
virtual_network_id = azurerm_virtual_network.hub.id
registration_enabled = false
}
The pattern that scales: one private DNS zone per service type, hosted centrally in the connectivity subscription, linked to the hub VNet, with all spoke DNS flowing through a central resolver in the hub (Azure Firewall DNS proxy or Azure DNS Private Resolver). On premises clients forward the privatelink zones to that resolver over VPN or ExpressRoute. Do not create a zone per application or link zones to every spoke individually, it turns into an unmanageable web.
Enforcing It with Policy
Two policies close the loop. A deny policy on public network access per service type stops anyone creating exposed resources. And the DeployIfNotExists policies for private DNS zone groups automatically attach the right zone to every new private endpoint, so application teams cannot forget the DNS half. With those in place, developers create endpoints in their own subscriptions and everything just resolves.
Operational Notes
Give private endpoints their own subnet and remember network security groups only apply to them if you enable private endpoint network policies on the subnet. Watch cost at scale: each endpoint bills hourly plus data processing, and a thousand endpoints is real money, so shared services in the hub sometimes make more sense than per spoke duplication. And always test resolution from every network segment, including on premises, with nslookup before declaring a migration done. The number of incidents that end with “the branch office was still resolving the public IP” is not small.
Cheers
Osama
Leave a comment