The first three parts of this series were about resolving names. This one is about the thing everybody wants to resolve: a Private Endpoint.

I have split it deliberately. Private Link is a networking construct, and it works whether or not your DNS is correct - the connection simply goes to the wrong place. Most "my Private Endpoint is broken" tickets are actually DNS tickets, and you cannot debug those until you know what the network layer is doing. So Part 4 covers the plumbing, and Part 5 covers the name resolution built on top of it.

Every Azure PaaS service ships with a public endpoint. mystorage.blob.core.windows.net resolves to a public IP, and your VM reaches it by leaving the virtual network, crossing the internet-facing edge of the platform, and authenticating.

That is not automatically insecure - the traffic is TLS, the service requires a key or a token - but it is very hard to explain to an auditor, and it is impossible to control with the tools a network team actually owns. Three generations of answers exist:

GenerationMechanismWhat it gives you
IP firewallAllow-list of public IPs on the serviceWorks, but you are managing public IPs and NAT ranges by hand
Service endpointThe subnet's traffic reaches the service over the backbone, with its private IP presented as identityNo public egress, but the service keeps its public IP and public DNS name
Private endpointA NIC in your subnet, with a private IP, mapped to the serviceThe service now has an address inside your address space

Only the third one changes the shape of the network. That is why it also changes the shape of your DNS.

Service endpoints versus private endpoints

This comparison comes up in every design review, so here it is properly.

Service endpointPrivate endpoint
What is createdA route and an identity on a subnetA network interface with a private IP
Address used by the clientThe service's public IPA private IP from your subnet
DNS changes neededNoneYes - the whole reason Part 5 exists
Reachable from on-premisesNoYes, over VPN or ExpressRoute
Reachable from peered VNetsNoYes, regionally and globally peered
GranularityWhole service, per subnetOne specific resource, and one sub-resource of it
Data exfiltration controlWeak - any account of that service type is reachableStrong - only the resource you connected to
CostFreeHourly charge plus data processed

The exfiltration point is the one that usually settles the argument. With a service endpoint to Microsoft.Storage, a VM in that subnet can reach any storage account in Azure, including one in an attacker's subscription. With a private endpoint, it reaches the one account you wired up, and nothing else.

Note

Service endpoints are not deprecated, and they are still a sensible choice for high-volume, low-sensitivity traffic where you do not want a per-endpoint cost. Just do not present them as equivalent to Private Link.

Anatomy of a private endpoint

A private endpoint is not one object. It is a small graph, and knowing the pieces makes troubleshooting far quicker.

PieceTypeNotes
Private endpointMicrosoft.Network/privateEndpointsThe resource you create, in a subnet
Network interfaceMicrosoft.Network/networkInterfacesCreated for you, read-only, holds the private IP
Private endpoint connectionChild of the target resourceCarries the approval state
Private DNS zone groupprivateEndpoints/privateDnsZoneGroupsOptional, covered in Part 5

Things that follow from this model:

bash
az network private-endpoint create \
  --name pe-sa1-blob \
  --resource-group rg-app \
  --vnet-name vnet-spoke-a \
  --subnet snet-privateendpoints \
  --private-connection-resource-id "/subscriptions/<sub>/resourceGroups/rg-data/providers/Microsoft.Storage/storageAccounts/sa1" \
  --group-id blob \
  --connection-name conn-sa1-blob

Microsoft's diagram of the same idea shows the connection between a consumer virtual network and a Private Link resource:

A private endpoint connecting a consumer virtual network to a Private Link resource
A private endpoint connects a consumer virtual network to a Private Link resource through an approved connection

Source: What is a private endpoint? - © Microsoft, Microsoft Learn.

Sub-resources are the detail everyone misses

A private endpoint does not connect to a resource. It connects to one sub-resource (the groupId) of that resource. A storage account is not one endpoint - it is six or more.

ServiceSub-resources you can target
Azure Storageblob, file, queue, table, web, dfs (plus _secondary variants)
Azure SQL DatabasesqlServer
Azure Key Vaultvault
Azure Cosmos DBSql, MongoDB, Cassandra, Gremlin, Table, Analytical
Azure App Servicesites
Azure Container Registryregistry
Event Hubs / Service Busnamespace
Azure Monitorazuremonitor (a Private Link Scope, not the workspace)
Your own serviceempty - a Private Link service

Two practical consequences:

Warning

Azure Monitor is a trap for the unwary. You do not put a private endpoint on a Log Analytics workspace directly - you create an Azure Monitor Private Link Scope (AMPLS), attach the workspaces and Application Insights components to it, and put the private endpoint on the scope. Getting this wrong silently sends telemetry over the public internet.

The approval workflow

A private endpoint connection has a state, and only one of those states carries traffic.

StateMeaning
PendingCreated manually, waiting for the resource owner
ApprovedLive. Traffic flows
RejectedThe owner said no
DisconnectedThe owner removed the connection. The endpoint is now decoration - delete it

Approval is automatic when you hold Microsoft.<Provider>/<resourceType>/privateEndpointConnectionsApproval/action on the target. That is the normal case inside one team. Across teams, or across tenants, you use the manual flow: create the endpoint with a request message, the owner approves it from the target resource's Networking blade.

bash
# What is waiting for me on this storage account?
az network private-endpoint-connection list \
  --id "/subscriptions/<sub>/resourceGroups/rg-data/providers/Microsoft.Storage/storageAccounts/sa1" \
  --query "[].{name:name,state:properties.privateLinkServiceConnectionState.status,desc:properties.privateLinkServiceConnectionState.description}" \
  --output table

az network private-endpoint-connection approve \
  --id "<connection-id>" \
  --description "Approved for project X"
Note

A stuck Pending connection is not a DNS problem, but it looks exactly like one: the name resolves to the private IP and the TCP connection times out. Check the connection state before you touch a single DNS record.

Routing, NSGs, and what the portal will not show you

When a private endpoint is created, the platform injects a /32 route into the subnets that can reach it, with next hop type InterfaceEndpoint. It has a higher priority than any UDR you write, which is deliberate - the platform will not let a route table black-hole a private endpoint by accident.

Some behaviour worth knowing before it surprises you:

bash
az network vnet subnet update \
  --resource-group rg-app \
  --vnet-name vnet-spoke-a \
  --name snet-privateendpoints \
  --private-endpoint-network-policies Enabled

I put private endpoints in a dedicated subnet with a descriptive name. Not because the platform requires it - it does not - but because it gives the NSG something to be about, and it keeps the /32 routes in one recognisable range.

Turning off the public endpoint is a separate job

This is the mistake that survives audits and shows up in breach reports.

Creating a private endpoint does not close the public one. mystorage.blob.core.windows.net still resolves publicly, still accepts connections, and still honours its own firewall rules. Anyone with a key can reach it from anywhere.

You have to disable it explicitly, on the resource:

bash
az storage account update \
  --name sa1 \
  --resource-group rg-data \
  --public-network-access Disabled

az keyvault update \
  --name kv1 \
  --resource-group rg-data \
  --public-network-access Disabled

The property is not perfectly uniform across services - some use publicNetworkAccess, some have their own firewall model with a defaultAction, some need both. Two habits make this manageable:

Warning

Before you flip publicNetworkAccess to Disabled, work out what else talks to that resource. Deployment agents, backup services, the portal's own data plane blades, and anything running from a laptop will all stop working. That is the point - but it should be a decision, not a discovery.

Everything above assumes Microsoft is the provider. You can be the provider too.

A Private Link service is published in front of a Standard Load Balancer, and it lets consumers in other VNets, other subscriptions, or other tenants create private endpoints pointing at your service.

ConceptWhat it means for you
Standard Load BalancerMandatory front end. Basic SKU is not supported
NAT subnetA subnet whose IPs are used to NAT incoming Private Link traffic
AliasA globally unique moniker you hand to consumers instead of a resource ID
VisibilityWhich subscriptions may even see the service
Auto-approvalSubscriptions whose connection requests are approved without a human
Proxy protocol v2Optional - the only way to see the consumer's real source information
bash
az network private-link-service create \
  --name pls-myapi \
  --resource-group rg-provider \
  --vnet-name vnet-provider \
  --subnet snet-pls-nat \
  --lb-name lb-myapi \
  --lb-frontend-ip-configs "<frontend-ip-config-id>" \
  --location westeurope

The alias is what makes this practical for a SaaS vendor: you publish pls-myapi.<guid>.westeurope.azure.privatelinkservice, a customer creates a private endpoint against it, you approve, and neither side ever sees the other's address space.

Note

Because incoming traffic is NAT'd, your backend sees the NAT subnet's addresses, not the consumer's. If you need per-customer source attribution, enable TCP Proxy protocol v2 on the service and parse it in your application.

Cost, limits, and the things that bite

Private endpoints are cheap individually and surprisingly not-cheap at scale. You pay an hourly rate per endpoint plus per GB processed in and out. A landing zone that automatically creates six endpoints per storage account across forty spokes is a line item worth modelling before you commit.

Other constraints to design around:

That last point is the bridge to the next post. Two endpoints for the same service, in the same private DNS zone, produce two A records for one name. The client picks one. Half your traffic goes to a VNet that cannot route it, and it looks exactly like an intermittent network fault.

A checklist before you call it done

  1. Private endpoint created in a dedicated subnet, in the same region and subscription as the VNet.
  2. One endpoint per sub-resource you actually use - not one per resource.
  3. Connection state is Approved, not Pending or Disconnected.
  4. privateEndpointNetworkPolicies enabled if you intend NSGs or UDRs to apply.
  5. publicNetworkAccess disabled on the target resource, and enforced by policy.
  6. Static private IP where the address appears in any external configuration.
  7. Peering and on-premises connectivity in place for every network that needs to reach it.
  8. DNS - which is all of Part 5.

Coming next

You now have a NIC in your subnet with a private IP, and an application that still connects to a public address, because nothing has told it otherwise.

Part 5 covers the CNAME chain from the public name into the privatelink namespace, the exact zone name for each service, private DNS zone groups and why you should always use them, how to run this at scale with Azure Policy, and how to make the whole thing work from on-premises.

Enjoy!