Azure Blob Private Link Looked Configured But Wasn't: Three Misconfigs That Left Public Access Open
Diagnosed and fixed a blob storage Private Link setup where the private endpoint was in the wrong VNet, the DNS A record was in an orphaned zone, and public access was never disabled.
ON THIS PAGE
The portal said Private Link was configured. Private endpoint connections: 1. Network access scoped to selected virtual networks. On paper it looked locked down. In practice, the storage account was still reachable from the public internet and live portal VMs had no working private path to it.
This is the diagnostic chain that found three separate misconfigs, each one hiding the others.
Situation
<storage-account> is a StorageV2 account in westeurope, RA-GRS replication, used for image storage in the live portal subscription. The goal was to confirm that only VMs in the live portal VNet could reach the blob endpoint, and that public access was off.
The account had contributor rights assigned. A private endpoint was visible in the portal — connection count showed 1, status Approved. The networking tab showed public access scoped to selected virtual networks with one IP rule in place.
The ask was: is this actually working?
Investigation
First thing was to pull the live networking config rather than trust the portal summary.
az storage account show \
--name <storage-account> \
--resource-group <data-rg> \
--subscription <subscription-id> \
--query "{name:name, publicNetworkAccess:publicNetworkAccess, networkRuleSet:networkRuleSet, privateEndpointConnections:privateEndpointConnections}" \
--output json
Output:
{
"name": "<storage-account>",
"networkRuleSet": {
"bypass": "AzureServices",
"defaultAction": "Deny",
"ipRules": [
{
"action": "Allow",
"ipAddressOrRange": "<public-ip>"
}
],
"virtualNetworkRules": []
},
"privateEndpointConnections": [
{
"name": "<storage-account>.<guid>",
"privateEndpoint": {
"id": ".../Microsoft.Network/privateEndpoints/testprivateblobendpoint"
},
"privateLinkServiceConnectionState": {
"status": "Approved"
},
"provisioningState": "Succeeded"
}
],
"publicNetworkAccess": "Enabled"
}
Three things stood out immediately. publicNetworkAccess was Enabled. virtualNetworkRules was empty — no VNet service endpoint rules, only a public IP allow. And the private endpoint was named testprivateblobendpoint — the “test” prefix was a flag.
Next, I pulled the PE details to see which VNet it was actually in:
az network private-endpoint show \
--name testprivateblobendpoint \
--resource-group <data-rg> \
--subscription <subscription-id> \
--query "{name:name, subnet:subnet.id, customDnsConfigs:customDnsConfigs}" \
--output json
Output:
{
"customDnsConfigs": [],
"name": "testprivateblobendpoint",
"subnet": ".../virtualNetworks/<adb-vnet>/subnets/private-subnet"
}
The PE was in <adb-vnet> — the Databricks VNet — not the live portal VNet. And customDnsConfigs was empty, meaning no DNS auto-registration had occurred.
I confirmed the VNets were not peered:
az network vnet peering list \
--resource-group <portal-rg> \
--vnet-name <portal-vnet> \
--subscription <subscription-id> \
--output json
# []
az network vnet peering list \
--resource-group <data-rg> \
--vnet-name <adb-vnet> \
--subscription <subscription-id> \
--output json
# []
No peering on either side. Live portal VMs had no route to <databricks-pe-ip> — the private IP assigned to the Databricks PE.
Then I checked the private DNS zones:
az network private-dns zone list \
--subscription <subscription-id> \
--query "[?name=='privatelink.blob.core.windows.net']" \
--output json
Three zones came back across different resource groups:
<data-rg>—numberOfVirtualNetworkLinks: 0<integration-rg>—numberOfVirtualNetworkLinks: 1<portal-rg>—numberOfVirtualNetworkLinks: 1
I checked the VNet link on the <portal-rg> zone:
az network private-dns link vnet list \
--resource-group <portal-rg> \
--zone-name "privatelink.blob.core.windows.net" \
--subscription <subscription-id> \
--output json
The zone was linked to the live portal VNet. That was the right zone. Then I checked its record sets:
az network private-dns record-set list \
--resource-group <portal-rg> \
--zone-name "privatelink.blob.core.windows.net" \
--subscription <subscription-id> \
--output json
Only a SOA record. No A record for <storage-account>.
Then I checked the <data-rg> zone:
az network private-dns record-set list \
--resource-group <data-rg> \
--zone-name "privatelink.blob.core.windows.net" \
--subscription <subscription-id> \
--output json
This zone had the A record:
{
"aRecords": [{ "ipv4Address": "<databricks-pe-ip>" }],
"fqdn": "<storage-account>.privatelink.blob.core.windows.net.",
"name": "<storage-account>",
"metadata": {
"creator": "created by private endpoint testprivateblobendpoint with resource guid <resource-guid>"
}
}
The A record existed — auto-created when testprivateblobendpoint was provisioned — but it was in a zone with zero VNet links. No VNet could resolve it.
What Failed First
Three independent misconfigs, each one masking the others:
The PE was created in the wrong VNet. Whoever set up testprivateblobendpoint deployed it into the Databricks VNet, likely as a proof-of-concept or because they were working from a Databricks context at the time. The storage account showed “Private endpoint connections: 1, Approved” which looked correct in the portal, but the endpoint served the wrong workload.
The DNS A record ended up in an orphaned zone. Azure auto-created the A record in the DNS zone that existed in the same resource group as the storage account (<data-rg>). That zone had never been linked to any VNet. The portal-linked zone (<portal-rg>) had no record for this account. Live portal VMs querying DNS got back the public CNAME, not a private IP.
Public access was never disabled. With the two gaps above, traffic from live portal VMs was falling through to the public endpoint. The <public-ip> IP rule and AzureServices bypass meant the storage account was still reachable from outside. Private Link had been started but never completed.
The Fix
Created a new private endpoint in the correct VNet:
az network private-endpoint create \
--name blob-pe-portal-live \
--resource-group <portal-rg> \
--subscription <subscription-id> \
--vnet-name <portal-vnet> \
--subnet <portal-subnet> \
--private-connection-resource-id /subscriptions/<subscription-id>/resourceGroups/<data-rg>/providers/Microsoft.Storage/storageAccounts/<storage-account> \
--group-id blob \
--connection-name blob-portal-live-connection \
--output json
The PE provisioned with private IP <portal-pe-ip>, auto-approved, Succeeded.
Added the A record to the zone that is actually linked to the live portal VNet:
az network private-dns record-set a add-record \
--resource-group <portal-rg> \
--zone-name "privatelink.blob.core.windows.net" \
--record-set-name "<storage-account>" \
--ipv4-address <portal-pe-ip> \
--subscription <subscription-id>
Disabled public access:
az storage account update \
--name <storage-account> \
--resource-group <data-rg> \
--subscription <subscription-id> \
--public-network-access Disabled
Output confirmed:
{
"defaultAction": "Deny",
"name": "<storage-account>",
"publicNetworkAccess": "Disabled"
}
Validated from a VM inside the live portal VNet. nslookup <storage-account>.blob.core.windows.net returned <portal-pe-ip>. A browser request to the blob endpoint returned HTTP 400 — the storage service received the request and responded, confirming DNS resolution, TCP connectivity, and TLS all worked through the private path.
The existing testprivateblobendpoint in the Databricks VNet was left in place — it serves the Databricks workload through its own DNS zone.
Production Rule
A private endpoint showing “Approved” in the portal tells you the connection was accepted. It says nothing about whether VMs in your target VNet can actually reach it. The three things that must all be true simultaneously:
- The PE is in a subnet that is routable from your consumer VMs — same VNet, or a peered VNet
- The DNS zone with the A record is linked to the VNet your consumers are in
- Public access is disabled — otherwise the private path is just an option, not a requirement
Check all three before closing any private link ticket. The portal will show a green PE connection state whether one is true or all three are.
Discussion