Private Endpoints

Private PaaS access, no public exposure.

Advanced25 min · lesson 12 of 15

Every Azure PaaS service (Platform as a Service, meaning Microsoft runs the servers and you only rent the service) ships with a front door that opens onto a public street. Storage, SQL Database, Key Vault: each one gets a DNS name (Domain Name System, the internet's phone book that turns names into numeric addresses) that resolves to a public IP address (Internet Protocol, the number that identifies a machine on a network) anyone online can knock on. Access keys and firewall rules are the lock and the bouncer. The door itself still faces the street. A private endpoint bricks up that street entrance and cuts a new door straight into your own hallway. The service picks up a private IP address inside your own virtual network (VNet), and once you switch public access off, the only way in is through your building.

Underneath, a private endpoint is a network interface (NIC), the same kind of virtual network adapter a virtual machine (VM) gets. Azure drops that NIC into a subnet you choose and wires it, over Azure Private Link (Microsoft's internal plumbing between customer networks and Azure services), to one specific instance of a PaaS resource. Traffic to the NIC's private IP rides the Azure backbone and lands on your storage account or database without ever touching the public internet. The endpoint maps to a single resource instance, and even to a single sub-resource inside it (blob, file, sqlServer, vault). So a compromised VM in your VNet cannot turn that path around and push your data to an attacker-owned account on the same service. That instance-level scoping is the anti-exfiltration property older mechanisms never had.

Service endpoints vs. private endpoints (the exam's favorite trap)

Azure's older mechanism is the service endpoint. It is a setting on a subnet that sends traffic to a service over the Azure backbone and stamps it with your VNet's identity, so the service firewall recognizes it and lets it through. Three differences come up on the exam over and over. First, a service endpoint leaves the service listening on its public IP. The service stays reachable from the internet; you have added a trusted lane beside the public road, not closed the road. Second, service endpoints only work from the subnet where you turned them on. Machines on-premises arriving over VPN (virtual private network, an encrypted tunnel across the internet) or ExpressRoute (a private leased line into Azure), plus workloads in peered VNets, cannot use them. A private endpoint is an IP address in your own address space, so anything with a route to it can reach it: peered spokes, on-prem traffic over a gateway, all of it. Third, service endpoints are free, while private endpoints bill by the hour and by the gigabyte. When a question says *"on-premises must reach storage privately"* or *"remove all public exposure"*, the answer is a private endpoint every time.

Build one: pull a storage account inside your VNet

Start from the estate you already have. There is app-vnet with a data subnet, and a storage account called contosodata that currently answers the whole internet. The --group-id flag names the sub-resource the endpoint fronts. One endpoint serves exactly one sub-resource, so an account used for both blobs and file shares needs two endpoints.

create the private endpoint
# Confirm the current exposure -- Enabled means the street door is open
az storage account show -n contosodata -g data-rg \
--query publicNetworkAccess -o tsv
# Enabled
# (an empty result means the property was never set -- the default
# also allows public traffic, so treat null the same as Enabled)
# Drop a private endpoint for the blob sub-resource into the data subnet
az network private-endpoint create -g net-rg -n pe-contosodata-blob \
--vnet-name app-vnet --subnet data \
--private-connection-resource-id $(az storage account show -n contosodata -g data-rg --query id -o tsv) \
--group-id blob --connection-name contosodata-blob-conn
# Output (trimmed):
# {
# "name": "pe-contosodata-blob",
# "privateLinkServiceConnections": [{
# "privateLinkServiceConnectionState": { "status": "Approved" }
# }],
# "provisioningState": "Succeeded",
# "subnet": { "id": ".../app-vnet/subnets/data" }
# }

The connection came back Approved on its own because you already hold RBAC (role-based access control, Azure's permission system) rights on the target account. When a team creates an endpoint pointing at a resource they do *not* own, say a partner subscription reaching for your storage, the connection lands as Pending and the resource owner has to approve or reject it. Private Link ships with that consent step built in.

DNS: where private endpoints actually break

Creating the endpoint changes nothing for your applications yet, because they connect by name, not by IP. Azure handles the name with a CNAME chain (Canonical Name, a DNS record that says "this name is really an alias for that other name"). contosodata.blob.core.windows.net now aliases to contosodata.privatelink.blob.core.windows.net. Public resolvers follow the chain to the public IP. Inside your VNet, a private DNS zone named privatelink.blob.core.windows.net catches the second hop and answers with the endpoint's private IP instead. Same question, different answer depending on where you stand. The pattern has a name, split-horizon DNS, and it is why your connection strings keep working untouched and why TLS (Transport Layer Security, the encryption behind HTTPS) stays happy. The certificate is issued for *.blob.core.windows.net, so clients have to keep connecting by the public name and never by the raw IP.

wire up private DNS
# Create the zone Azure expects for blob endpoints (exact name matters)
az network private-dns zone create -g net-rg \
-n "privatelink.blob.core.windows.net"
# Link the zone to the VNet so its VMs resolve through it
az network private-dns link vnet create -g net-rg \
--zone-name "privatelink.blob.core.windows.net" \
-n app-vnet-link --virtual-network app-vnet --registration-enabled false
# Attach a dns-zone-group so the endpoint manages its own A record
az network private-endpoint dns-zone-group create -g net-rg \
--endpoint-name pe-contosodata-blob -n default \
--private-dns-zone "privatelink.blob.core.windows.net" --zone-name blob
# Verify the record landed
az network private-dns record-set a list -g net-rg \
-z "privatelink.blob.core.windows.net" \
--query "[].{name:name, ip:aRecords[0].ipv4Address}" -o table
# Name Ip
# ----------- ---------
# contosodata 10.20.2.5

The dns-zone-group is the piece most tutorials skip. It hands the endpoint ownership of its own A record, so if you rebuild the endpoint and it picks up a new IP, the zone corrects itself. Nearly every private-endpoint outage in production turns out to be DNS rather than networking: a zone nobody linked to the VNet, or on-premises resolvers still handing back the public IP because nothing forwards the privatelink domains into Azure (through Azure DNS Private Resolver, or a conditional forwarder aimed at the VNet).

Close the public door, then prove it from both sides

flip to private-only and verify
# From a VM inside app-vnet: who answers for the name now?
nslookup contosodata.blob.core.windows.net
# contosodata.blob.core.windows.net canonical name = contosodata.privatelink.blob.core.windows.net.
# Name: contosodata.privatelink.blob.core.windows.net
# Address: 10.20.2.5 <-- private IP: the zone answered
# Close the public door
az storage account update -g data-rg -n contosodata \
--public-network-access Disabled
# From your laptop (outside the VNet), the name still resolves publicly...
nslookup contosodata.blob.core.windows.net
# Address: 20.60.153.33 <-- public IP, but the service refuses it:
az storage blob list --account-name contosodata -c logs \
--auth-mode login -o table
# This request is not authorized to perform this operation.
# RequestId:8f2c...
# Time:2026-07-13T09:41:07Z
# ErrorCode:AuthorizationFailure
# The same command from the VM inside the VNet lists blobs normally.

Check both sides, every time. Inside the VNet you want the private IP and a working blob list. Outside you want AuthorizationFailure. That failure is the entire point. From here on, a leaked access key buys an attacker on the internet nothing, because there is no network path left to spend it on. Authentication guards *who*. The private endpoint guards *from where*.

A private endpoint on its own does not remove public access
Creating the endpoint adds a private path. It does not close the public one. While publicNetworkAccess is still Enabled, or was never set at all (null behaves the same way), the service stays wide open to the internet and your "private" architecture is decoration. Pair the endpoint with --public-network-access Disabled, or with a deny-by-default rule plus explicit exceptions, then test from outside the VNet to prove the door really is shut.

Cost, limits, and running this at scale

Private endpoints cost money. Figure roughly a cent an hour each, about $7 to $8 a month, plus another cent or so per gigabyte processed in each direction. One endpoint is rounding error. Four hundred across an estate is a real line item, and remember the count is one per sub-resource, so blob *and* file on a single account means two. Each NIC also eats an IP address out of your subnet. Two operational edges are worth writing down. NSG rules (network security group, the packet filter you attach to a subnet or NIC) only apply to private-endpoint traffic when the subnet's privateEndpointNetworkPolicies setting is enabled, and it is *Disabled* by default, so check it with az network vnet subnet show instead of assuming. And every service brings its own zone name: privatelink.vaultcore.azure.net for Key Vault, privatelink.database.windows.net for SQL. Mature estates therefore host all the zones once in a hub VNet, link every spoke to them, and use Azure Policy (from the governance lesson) to deny public access and auto-attach dns-zone-groups on new resources. That turns "private by default" from a habit somebody has to remember into a property of the platform.

connection approvals
# List every private-endpoint connection to a resource you own
STG_ID=$(az storage account show -n contosodata -g data-rg --query id -o tsv)
az network private-endpoint-connection list --id $STG_ID \
--query "[].{name:name, status:properties.privateLinkServiceConnectionState.status}" -o table
# Name Status
# ---------------------- --------
# contosodata-blob-conn Approved
# analytics-team-pe-conn Pending
# Approve (or reject) the partner team's request
az network private-endpoint-connection approve \
--id <connection-resource-id> \
--description "Approved: analytics team read path"

Treat that Pending queue like an access review. Every approved connection is a standing network path into your data, so write down who asked for it and why, then re-read the list whenever teams reorganize or get decommissioned.

That closes the networking arc: private subnets for workloads, NSGs filtering the flows, peering and gateways stitching networks together, Bastion for operator access, and now PaaS services pulled inside the same perimeter. An architecture is only private on the day you tested it. Proving it *stays* private is a telemetry problem. Next up, Azure Monitor and Log Analytics hands you the instruments: KQL queries (Kusto Query Language, the query language Azure uses for logs) across storage and network logs so you can see exactly which requests arrived from where, plus alerts that fire the moment somebody flips publicNetworkAccess back to Enabled.

Private Link is how you stop shipping storage, SQL, Key Vault, and ACR (Azure Container Registry, where your container images live) traffic across the public internet. The private endpoint is a NIC. The privatelink DNS zone points the normal hostname at that NIC's private IP for clients inside your network. Get the DNS wrong and applications time out while the portal keeps insisting everything looks fine.

Defense in depth here is three things stacked: the endpoint, public access disabled, and RBAC on top. Service endpoints are older and still earn their place in some designs, but private endpoints are the default recommendation for sensitive PaaS. For the exam, remember that private endpoints consume subnet IP addresses and that each service has its own sub-resource type (blob, vault, sqlServer, and so on).

Try this

Grab a storage account, or make one, turn off public network access, and create a private endpoint into a lab subnet. Then resolve the privatelink DNS name and check that it comes back with a 10.x address.

terminal
RG=rg-lab-pe
# assumes vnet-lab / snet-data from earlier lab or create quickly
az network private-endpoint list -g $RG -o table 2>/dev/null
SA=$(az storage account list -g $RG --query "[0].name" -o tsv)
az storage account update -g $RG -n $SA --public-network-access Disabled
# After PE + private DNS zone are linked:
nslookup $SA.blob.core.windows.net
# Expect a privatelink CNAME and a private A record in 10.x
output
$ nslookup contosolabsa1234.blob.core.windows.net
Name: contosolabsa1234.privatelink.blob.core.windows.net
Address: 10.10.2.4
# Sample output — public access Disabled means the public endpoint refuses connections even with a valid key.

Takeaway

A private endpoint gives a PaaS service a NIC and a private IP address inside your VNet. Disabling public access closes the old front door, so a leaked key is worthless to anyone standing out on the internet.

Next: attach the matching privatelink private DNS zone to the VNet. Skip it and clients keep resolving the public name, then fail in ways that are genuinely painful to debug.

Quick check
01You create a private endpoint for contosodata's blob sub-resource, link the privatelink.blob.core.windows.net zone, and confirm that VMs in the VNet now resolve the private IP 10.20.2.5. A client out on the public internet can still reach the storage account. What went wrong?
Correct — The endpoint is additive, so the public listener stays up until you switch it off.
Incorrect — On-prem resolver forwarding decides who resolves the private IP. It has nothing to do with whether the public endpoint is listening.
Incorrect — The create output showed the connection Approved, and approval state does not control public access either way.
Incorrect — Rotating keys changes credentials, not the network path. The public endpoint would still answer.
02The lesson lines service endpoints up against private endpoints. Which statement describes a real difference between the two?
Incorrect — Backwards. The private endpoint is the one that gets a private IP; the service endpoint does the tagging.
Incorrect — Service endpoints are free, and they involve no private DNS zone at all.
Incorrect — Also backwards. Service endpoints leave the public IP listening.
Correct — and that scope limit is exactly why on-prem scenarios point at private endpoints.
03A compliance rule says an Azure SQL Database must be reachable only from inside the corporate network, including staff on-premises arriving over ExpressRoute, with no public network path at all. Which approach fits best?
Correct — Private path, the right DNS zone, and the public door shut.
Incorrect — Service endpoints do not serve on-premises traffic over ExpressRoute, and the public endpoint stays up.
Incorrect — That is still a public network path, which the rule forbids outright.
Incorrect — Wrong service entirely, and it does nothing for SQL or for on-prem reach.

Related