Azure Private Endpoint Guide
Secure PaaS services with Private Endpoints: DNS resolution, cross-region, hub-spoke, and troubleshooting.
Prerequisites
- Understanding of Azure VNet concepts (subnets, NSGs, peering)
- Familiarity with DNS resolution fundamentals
What Are Azure Private Endpoints?
Azure Private Endpoints provide private connectivity from your virtual network (VNet) to Azure PaaS services (Storage, SQL Database, Cosmos DB, Key Vault, etc.) without traversing the public internet. A private endpoint is a network interface with a private IP address in your VNet that connects to a specific Azure service. Traffic between your VNet and the service travels entirely over the Microsoft backbone network.
Without private endpoints, applications in your VNet communicate with Azure PaaS services over their public endpoints. Even though this traffic is encrypted with TLS, it routes over the public internet, which may violate compliance requirements, expose the service to public discovery, and create a larger attack surface. Private endpoints eliminate these concerns by keeping all traffic private and removing the service's public endpoint entirely.
This guide covers private endpoint architecture, DNS resolution (the trickiest part), cross-region and cross-VNet configurations, Terraform automation, troubleshooting common issues, and integration with hub-spoke network topologies.
Service Endpoints vs Private Endpoints
Service Endpoints keep traffic on the Azure backbone but use the service's public IP. They are simpler and free but do not provide a private IP. Private Endpoints assign a private IP in your VNet, enabling access from on-premises via VPN/ExpressRoute and providing full DNS-level isolation. Use Private Endpoints for production workloads requiring compliance; use Service Endpoints for simpler scenarios where cost matters.
Creating a Private Endpoint
Creating a private endpoint involves three components: the private endpoint resource itself, a network interface with a private IP, and a private DNS zone for name resolution. Without proper DNS configuration, applications will still resolve to the service's public IP, bypassing the private endpoint entirely.
# Create a private endpoint for Azure SQL Database
az network private-endpoint create \
--name "sql-private-endpoint" \
--resource-group myapp-rg \
--vnet-name myapp-vnet \
--subnet private-endpoints-subnet \
--private-connection-resource-id "/subscriptions/SUB_ID/resourceGroups/myapp-rg/providers/Microsoft.Sql/servers/myapp-sql" \
--group-id sqlServer \
--connection-name "sql-pe-connection"
# Get the private IP assigned to the endpoint
az network private-endpoint show \
--name sql-private-endpoint \
--resource-group myapp-rg \
--query 'customDnsConfigs[].{FQDN:fqdn, IP:ipAddresses[0]}' \
--output table
# Disable public access on the SQL Server
az sql server update \
--name myapp-sql \
--resource-group myapp-rg \
--public-network-access DisabledDNS Resolution: The Critical Piece
DNS is the most complex and error-prone aspect of private endpoints. When you create a private endpoint, Azure does NOT automatically update DNS. Your application must resolve the service's FQDN (e.g., myapp-sql.database.windows.net) to the private IP address. Without this, the application will connect via the public endpoint, which defeats the purpose of the private endpoint.
Azure provides Private DNS Zones that integrate with your VNet to provide automatic DNS resolution. Each Azure service has a specific private DNS zone name that you must create and link to your VNet.
Private DNS Zone Names by Service
| Service | Private DNS Zone | Group ID |
|---|---|---|
| Azure SQL | privatelink.database.windows.net | sqlServer |
| Blob Storage | privatelink.blob.core.windows.net | blob |
| Key Vault | privatelink.vaultcore.azure.net | vault |
| Cosmos DB | privatelink.documents.azure.com | Sql |
| Azure Container Registry | privatelink.azurecr.io | registry |
| Azure App Service | privatelink.azurewebsites.net | sites |
| Event Hubs | privatelink.servicebus.windows.net | namespace |
# Create the private DNS zone
az network private-dns zone create \
--name "privatelink.database.windows.net" \
--resource-group myapp-rg
# Link the DNS zone to your VNet
az network private-dns zone vnet-link create \
--name "sql-dns-link" \
--zone-name "privatelink.database.windows.net" \
--resource-group myapp-rg \
--virtual-network myapp-vnet \
--registration-enabled false
# Create DNS records for the private endpoint
az network private-endpoint dns-zone-group create \
--endpoint-name sql-private-endpoint \
--resource-group myapp-rg \
--name "sql-dns-group" \
--private-dns-zone "/subscriptions/SUB_ID/resourceGroups/myapp-rg/providers/Microsoft.Network/privateDnsZones/privatelink.database.windows.net" \
--zone-name "sql"
# Verify DNS resolution from inside the VNet
# nslookup myapp-sql.database.windows.net
# Should return: myapp-sql.privatelink.database.windows.net -> 10.0.3.5 (private IP)DNS Resolution from On-Premises
If you access Azure services from on-premises via VPN or ExpressRoute, you must configure DNS forwarding. On-premises DNS servers need to forward queries for*.privatelink.database.windows.net to Azure DNS (168.63.129.16) through a DNS forwarder VM in your VNet. Without this, on-premises clients will resolve public IPs and bypass the private endpoint.
Terraform Automation
# Private endpoint for Azure SQL Database
resource "azurerm_private_endpoint" "sql" {
name = "sql-private-endpoint"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
subnet_id = azurerm_subnet.private_endpoints.id
private_service_connection {
name = "sql-pe-connection"
private_connection_resource_id = azurerm_mssql_server.main.id
subresource_names = ["sqlServer"]
is_manual_connection = false
}
private_dns_zone_group {
name = "sql-dns-group"
private_dns_zone_ids = [azurerm_private_dns_zone.sql.id]
}
tags = {
Environment = "production"
}
}
# Private DNS zone
resource "azurerm_private_dns_zone" "sql" {
name = "privatelink.database.windows.net"
resource_group_name = azurerm_resource_group.main.name
}
# Link DNS zone to VNet
resource "azurerm_private_dns_zone_virtual_network_link" "sql" {
name = "sql-dns-link"
resource_group_name = azurerm_resource_group.main.name
private_dns_zone_name = azurerm_private_dns_zone.sql.name
virtual_network_id = azurerm_virtual_network.main.id
registration_enabled = false
}
# Disable public access
resource "azurerm_mssql_server" "main" {
name = "myapp-sql"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
version = "12.0"
administrator_login = "sqladmin"
administrator_login_password = var.sql_admin_password
public_network_access_enabled = false
}
# Subnet for private endpoints (no NSG delegation issues)
resource "azurerm_subnet" "private_endpoints" {
name = "private-endpoints"
resource_group_name = azurerm_resource_group.main.name
virtual_network_name = azurerm_virtual_network.main.name
address_prefixes = ["10.0.3.0/24"]
private_endpoint_network_policies = "Enabled"
}Cross-Region Private Endpoints
Private endpoints can connect to Azure services in different regions than the VNet. For example, a VNet in East US can have a private endpoint connecting to a Cosmos DB account in West Europe. This is useful for disaster recovery scenarios and accessing globally distributed services from a specific region.
# Create a private endpoint in East US for a resource in West Europe
az network private-endpoint create \
--name "cosmosdb-cross-region-pe" \
--resource-group myapp-rg \
--vnet-name eastus-vnet \
--subnet private-endpoints-subnet \
--private-connection-resource-id "/subscriptions/SUB_ID/resourceGroups/myapp-rg/providers/Microsoft.DocumentDB/databaseAccounts/mycosmosdb" \
--group-id Sql \
--connection-name "cosmosdb-pe-connection" \
--location eastus
# Note: the resource (Cosmos DB) can be in any region
# The private endpoint and its IP are in the VNet's regionHub-Spoke Integration
In hub-spoke network topologies, private endpoints are typically created in the hub VNet with centralized DNS zones. Spoke VNets use VNet peering to access the hub, and DNS zones are linked to all spoke VNets for name resolution.
# Centralized DNS in hub VNet
resource "azurerm_private_dns_zone_virtual_network_link" "spoke1" {
name = "spoke1-link"
resource_group_name = azurerm_resource_group.hub.name
private_dns_zone_name = azurerm_private_dns_zone.sql.name
virtual_network_id = azurerm_virtual_network.spoke1.id
registration_enabled = false
}
resource "azurerm_private_dns_zone_virtual_network_link" "spoke2" {
name = "spoke2-link"
resource_group_name = azurerm_resource_group.hub.name
private_dns_zone_name = azurerm_private_dns_zone.sql.name
virtual_network_id = azurerm_virtual_network.spoke2.id
registration_enabled = false
}
# DNS forwarder for on-premises resolution
resource "azurerm_linux_virtual_machine" "dns_forwarder" {
name = "dns-forwarder"
resource_group_name = azurerm_resource_group.hub.name
location = azurerm_resource_group.hub.location
size = "Standard_B2s"
admin_username = "azureuser"
network_interface_ids = [azurerm_network_interface.dns_forwarder.id]
# Configure as DNS forwarder with conditional forwarding
# for privatelink.* zones to 168.63.129.16
}Troubleshooting Private Endpoints
Most private endpoint issues stem from DNS misconfiguration. Here is a systematic troubleshooting approach.
# Step 1: Verify the private endpoint is in Approved state
az network private-endpoint show \
--name sql-private-endpoint \
--resource-group myapp-rg \
--query 'privateLinkServiceConnections[0].privateLinkServiceConnectionState.status'
# Expected: "Approved"
# Step 2: Check the private IP assignment
az network private-endpoint show \
--name sql-private-endpoint \
--resource-group myapp-rg \
--query 'customDnsConfigs[].ipAddresses[]' --output tsv
# Expected: 10.0.3.x (private IP in your subnet)
# Step 3: Verify DNS resolution from inside the VNet
# From a VM in the VNet:
nslookup myapp-sql.database.windows.net
# Expected: myapp-sql.privatelink.database.windows.net -> 10.0.3.x
# Step 4: Check if DNS zone is linked to the VNet
az network private-dns zone vnet-link list \
--zone-name "privatelink.database.windows.net" \
--resource-group myapp-rg \
--query '[].{Name:name, VNet:virtualNetwork.id, Status:provisioningState}'
# Step 5: Test connectivity
# From a VM in the VNet:
# Test-NetConnection -ComputerName myapp-sql.database.windows.net -Port 1433
# Expected: TcpTestSucceeded: True
# Common issues:
# 1. DNS resolves to public IP -> DNS zone not linked to VNet
# 2. Connection timeout -> NSG blocking traffic to private endpoint subnet
# 3. "Pending" state -> Manual approval required on the service
# 4. Works from VNet but not on-premises -> DNS forwarding not configuredNSG and Security Considerations
By default, Network Security Groups (NSGs) do not apply to private endpoint traffic. To enable NSG filtering on private endpoint traffic, you must enable thePrivateEndpointNetworkPolicies property on the subnet. This is important for organizations that require microsegmentation.
# Enable NSG enforcement on private endpoint subnet
az network vnet subnet update \
--name private-endpoints \
--vnet-name myapp-vnet \
--resource-group myapp-rg \
--private-endpoint-network-policies Enabled
# Create NSG rule to restrict private endpoint access
az network nsg rule create \
--nsg-name pe-subnet-nsg \
--resource-group myapp-rg \
--name "allow-app-to-sql" \
--priority 100 \
--direction Inbound \
--access Allow \
--source-address-prefixes "10.0.1.0/24" \
--destination-address-prefixes "10.0.3.0/24" \
--destination-port-ranges 1433 \
--protocol TcpUse Azure Policy for Enforcement
Use Azure Policy to enforce private endpoint usage across your organization. The built-in policy "Azure services should use Private Link" audits or denies resources without private endpoints. Combine this with "Configure services to use private DNS zones" to automate DNS configuration. This ensures no team accidentally creates PaaS resources with public endpoints.
Key Takeaways
- 1Private Endpoints assign a private IP in your VNet to Azure PaaS services, eliminating public exposure.
- 2DNS configuration is the most critical and error-prone aspect of private endpoint setup.
- 3Each Azure service has a specific privatelink DNS zone name that must be created and linked.
- 4On-premises access requires DNS forwarding through a forwarder VM to Azure DNS (168.63.129.16).
Frequently Asked Questions
What is the difference between Service Endpoints and Private Endpoints?
Why does my application still connect via the public endpoint after creating a Private Endpoint?
Written by CloudToolStack Team
Cloud engineers and architects with hands-on experience across AWS, Azure, and GCP. We write guides based on real-world production patterns, not just documentation rewrites.
Disclaimer: This guide is for educational purposes. Cloud services change frequently; always refer to official documentation for the latest information. AWS, Azure, and GCP are trademarks of their respective owners.