6 min read
Private Endpoints Everywhere: Securing Your Azure Data Platform
Private endpoints are the foundation of secure Azure architectures. By bringing Azure services into your virtual network, you eliminate exposure to the public internet while maintaining full functionality.
Understanding Private Endpoints
A private endpoint:
- Creates a network interface with a private IP in your VNet
- Maps to a specific Azure resource (or sub-resource)
- Enables DNS resolution to the private IP
- Blocks public access when properly configured
Architecture Overview
┌─────────────────────────────────────────────────────────┐
│ Virtual Network │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ PE: Storage │ │ PE: SQL DB │ │ PE: Key Vault│ │
│ │ 10.0.1.4 │ │ 10.0.1.5 │ │ 10.0.1.6 │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ ┌──────┴────────────────┴────────────────┴──────┐ │
│ │ Private DNS Zones │ │
│ │ privatelink.blob.core.windows.net │ │
│ │ privatelink.database.windows.net │ │
│ │ privatelink.vaultcore.azure.net │ │
│ └────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────┴────────────────────────┐ │
│ │ Data Factory / Databricks │ │
│ │ (Managed VNet or VNet Injected) │ │
│ └────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
Creating Private Endpoints
Azure Storage
# Create private endpoint for blob
az network private-endpoint create \
--name storage-pe \
--resource-group myRG \
--vnet-name myVNet \
--subnet data-subnet \
--private-connection-resource-id "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Storage/storageAccounts/mystorageaccount" \
--group-id blob \
--connection-name storage-connection
# Create private endpoint for dfs (Data Lake)
az network private-endpoint create \
--name storage-dfs-pe \
--resource-group myRG \
--vnet-name myVNet \
--subnet data-subnet \
--private-connection-resource-id "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Storage/storageAccounts/mystorageaccount" \
--group-id dfs \
--connection-name storage-dfs-connection
Azure SQL Database
az network private-endpoint create \
--name sql-pe \
--resource-group myRG \
--vnet-name myVNet \
--subnet data-subnet \
--private-connection-resource-id "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Sql/servers/mysqlserver" \
--group-id sqlServer \
--connection-name sql-connection
Azure Key Vault
az network private-endpoint create \
--name keyvault-pe \
--resource-group myRG \
--vnet-name myVNet \
--subnet data-subnet \
--private-connection-resource-id "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.KeyVault/vaults/mykeyvault" \
--group-id vault \
--connection-name keyvault-connection
Terraform Configuration
# Private Endpoint Module
module "private_endpoint" {
source = "./modules/private_endpoint"
for_each = {
storage_blob = {
resource_id = azurerm_storage_account.main.id
subresource_name = "blob"
dns_zone_id = azurerm_private_dns_zone.blob.id
}
storage_dfs = {
resource_id = azurerm_storage_account.main.id
subresource_name = "dfs"
dns_zone_id = azurerm_private_dns_zone.dfs.id
}
sql = {
resource_id = azurerm_mssql_server.main.id
subresource_name = "sqlServer"
dns_zone_id = azurerm_private_dns_zone.sql.id
}
keyvault = {
resource_id = azurerm_key_vault.main.id
subresource_name = "vault"
dns_zone_id = azurerm_private_dns_zone.vault.id
}
}
name = "pe-${each.key}"
location = var.location
resource_group_name = var.resource_group_name
subnet_id = azurerm_subnet.data.id
resource_id = each.value.resource_id
subresource_name = each.value.subresource_name
dns_zone_id = each.value.dns_zone_id
}
# Module definition
resource "azurerm_private_endpoint" "this" {
name = var.name
location = var.location
resource_group_name = var.resource_group_name
subnet_id = var.subnet_id
private_service_connection {
name = "${var.name}-connection"
private_connection_resource_id = var.resource_id
is_manual_connection = false
subresource_names = [var.subresource_name]
}
private_dns_zone_group {
name = "dns-zone-group"
private_dns_zone_ids = [var.dns_zone_id]
}
}
DNS Configuration
Private DNS Zones
# Create private DNS zones
zones=(
"privatelink.blob.core.windows.net"
"privatelink.dfs.core.windows.net"
"privatelink.database.windows.net"
"privatelink.vaultcore.azure.net"
"privatelink.datafactory.azure.net"
"privatelink.adf.azure.com"
"privatelink.azuredatabricks.net"
)
for zone in "${zones[@]}"; do
az network private-dns zone create \
--resource-group myRG \
--name "$zone"
# Link to VNet
az network private-dns link vnet create \
--resource-group myRG \
--zone-name "$zone" \
--name "vnet-link" \
--virtual-network myVNet \
--registration-enabled false
done
DNS Zone Configuration (Terraform)
locals {
dns_zones = {
blob = "privatelink.blob.core.windows.net"
dfs = "privatelink.dfs.core.windows.net"
sql = "privatelink.database.windows.net"
vault = "privatelink.vaultcore.azure.net"
datafactory = "privatelink.datafactory.azure.net"
synapse = "privatelink.sql.azuresynapse.net"
databricks = "privatelink.azuredatabricks.net"
}
}
resource "azurerm_private_dns_zone" "zones" {
for_each = local.dns_zones
name = each.value
resource_group_name = var.resource_group_name
}
resource "azurerm_private_dns_zone_virtual_network_link" "links" {
for_each = local.dns_zones
name = "vnet-link-${each.key}"
resource_group_name = var.resource_group_name
private_dns_zone_name = azurerm_private_dns_zone.zones[each.key].name
virtual_network_id = azurerm_virtual_network.main.id
registration_enabled = false
}
Disabling Public Access
After private endpoints are configured, disable public access:
Storage Account
az storage account update \
--name mystorageaccount \
--resource-group myRG \
--public-network-access Disabled
Azure SQL
az sql server update \
--name mysqlserver \
--resource-group myRG \
--public-network-access Disabled
Key Vault
az keyvault update \
--name mykeyvault \
--resource-group myRG \
--public-network-access Disabled
Private Endpoints for Data Services
Azure Databricks
resource "azurerm_databricks_workspace" "this" {
name = "databricks-private"
resource_group_name = var.resource_group_name
location = var.location
sku = "premium"
public_network_access_enabled = false
network_security_group_rules_required = "NoAzureDatabricksRules"
custom_parameters {
no_public_ip = true
virtual_network_id = azurerm_virtual_network.main.id
private_subnet_name = azurerm_subnet.databricks_private.name
public_subnet_name = azurerm_subnet.databricks_public.name
private_subnet_network_security_group_association_id = azurerm_subnet_network_security_group_association.private.id
public_subnet_network_security_group_association_id = azurerm_subnet_network_security_group_association.public.id
}
}
# Private endpoints for Databricks
resource "azurerm_private_endpoint" "databricks_ui" {
name = "pe-databricks-ui"
location = var.location
resource_group_name = var.resource_group_name
subnet_id = azurerm_subnet.endpoints.id
private_service_connection {
name = "databricks-ui-connection"
private_connection_resource_id = azurerm_databricks_workspace.this.id
subresource_names = ["databricks_ui_api"]
is_manual_connection = false
}
}
Azure Synapse Analytics
resource "azurerm_synapse_workspace" "this" {
name = "synapse-private"
resource_group_name = var.resource_group_name
location = var.location
storage_data_lake_gen2_filesystem_id = azurerm_storage_data_lake_gen2_filesystem.synapse.id
sql_administrator_login = var.sql_admin_user
sql_administrator_login_password = var.sql_admin_password
managed_virtual_network_enabled = true
identity {
type = "SystemAssigned"
}
}
# Private endpoints for Synapse
resource "azurerm_synapse_managed_private_endpoint" "storage" {
name = "pe-storage"
synapse_workspace_id = azurerm_synapse_workspace.this.id
target_resource_id = azurerm_storage_account.main.id
subresource_name = "dfs"
}
Testing Connectivity
# Python script to test private endpoint connectivity
import socket
import dns.resolver
def test_private_dns(hostname):
"""Verify DNS resolves to private IP"""
try:
answers = dns.resolver.resolve(hostname, 'A')
for rdata in answers:
ip = str(rdata)
print(f"{hostname} -> {ip}")
# Check if IP is private
if ip.startswith(('10.', '172.', '192.168.')):
print(" [OK] Resolves to private IP")
else:
print(" [WARN] Resolves to public IP")
except Exception as e:
print(f"DNS resolution failed: {e}")
# Test your resources
test_private_dns("mystorageaccount.blob.core.windows.net")
test_private_dns("mysqlserver.database.windows.net")
test_private_dns("mykeyvault.vault.azure.net")
Network Security Groups
resource "azurerm_network_security_group" "private_endpoints" {
name = "nsg-private-endpoints"
location = var.location
resource_group_name = var.resource_group_name
# Allow all VNet traffic
security_rule {
name = "AllowVNetInbound"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "*"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = "VirtualNetwork"
destination_address_prefix = "VirtualNetwork"
}
# Deny all other inbound
security_rule {
name = "DenyAllInbound"
priority = 4096
direction = "Inbound"
access = "Deny"
protocol = "*"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = "*"
destination_address_prefix = "*"
}
}
Best Practices
- Create DNS zones first: Ensure DNS is ready before creating endpoints
- Use consistent naming: pe-{service}-{resource}
- Disable public access: After verifying private connectivity
- Monitor endpoint health: Use Azure Monitor for connection status
- Document subnet usage: Track which subnets host which endpoints
- Use managed identities: Combine with private endpoints for credential-free access
Conclusion
Private endpoints are essential for secure Azure data platforms:
- Eliminate public internet exposure
- Enable compliance with data residency requirements
- Reduce attack surface
- Maintain full functionality
Plan your private endpoint strategy early - retrofitting is possible but adds complexity.