DEV Community

Shivanna Gundanavar
Shivanna Gundanavar

Posted on

Your Azure Private Endpoint Is Approved. So Why Does It Still Resolve a Public IP?

You disable public access on a storage account. You add a private endpoint. The portal shows the connection as Approved. You deploy the app.

It fails with a timeout or a 403.

Nothing in your pipeline warned you. what-if showed the right resources, the deployment succeeded, and every resource is green. The problem is that these tools check resources. They do not check whether a name inside your network actually resolves to the private address.

In my experience, most private connectivity problems in Azure are not firewall problems. They are DNS problems. This post explains why, and shares a small script that checks the whole path before you deploy.

What must be true for private access to work

For a workload in a VNet to reach a PaaS service privately, all of these must hold:

Requirement What happens if it is missing

1 The private endpoint connection is Approved Connection stays pending, nothing works
2 The matching privatelink private DNS zone exists The name resolves to the public address
3 The zone is linked to the VNet doing the lookup Same: public address, even though the zone exists
4 The zone has an A record for the resource Lookup fails or falls back to public
5 If the VNet uses custom DNS, it forwards privatelink zones to Azure DNS Lookup never reaches the private zone

Number 3 is the classic trap. The zone exists, the endpoint is approved, the A record is there, and it still does not work, because the VNet that runs the lookup cannot see the zone.

A quick manual test from a VM inside the VNet:

bash
nslookup mystore.dfs.core.windows.net

If the answer is a 10.x address, DNS is fine. If it is a public address, one of the five items above is broken, and you now have to find which one.

The idea: a what-if for network paths

Finding the broken item by clicking through the portal takes a long time, especially when the zone lives in another subscription or resource group. So I wrote a script that asks the one question directly:

Can workloads in this source VNet reach this target resource privately?

It reads the configuration with the Azure CLI and reports PASS, WARN, or FAIL for each requirement, with a suggested fix.

Example output when the zone exists but is not linked to the source VNet:

text
[PASS] Public access: disabled
[PASS] Private endpoint: 1 approved connection(s)
[PASS] Source VNet DNS: uses Azure-provided DNS
[PASS] DNS zone for pe-mystore-dfs [dfs]: privatelink.dfs.core.windows.net exists
[FAIL] VNet link for pe-mystore-dfs [dfs]: privatelink.dfs.core.windows.net is not linked to the source VNet
Fix: Link the zone to the source VNet (or to the hub VNet that provides DNS resolution for it).
[PASS] A record for pe-mystore-dfs [dfs]: mystore found

The exit code is 1 when anything fails, so a pipeline can block the change.

How it works

The script makes five groups of read-only az calls:

az resource show on the target: read publicNetworkAccess and privateEndpointConnections.
az network private-endpoint show: read the group id (blob, dfs, vault, sqlServer, and so on).
A lookup table maps the group id to the expected zone, for example dfs to privatelink.dfs.core.windows.net.
az network private-dns link vnet list: is the source VNet in the zone's links?
az network private-dns record-set a list: is there a record named after the resource?

It also reads the source VNet's DNS servers. If it uses custom DNS, the script raises a warning, because it cannot see inside your DNS servers and only you can confirm they forward privatelink zones to Azure DNS (168.63.129.16) or to an Azure DNS Private Resolver.

The core check, in a few lines:

python
links = az(["network", "private-dns", "link", "vnet", "list",
"-g", zone_rg, "-z", zone]) or []
linked = any(
((l.get("virtualNetwork") or {}).get("id") or "").lower() == source_vnet_id.lower()
for l in links
)
if not linked:
report.add("FAIL", "VNet link", f"{zone} is not linked to the source VNet",
"Link the zone to the source VNet or to the hub VNet that provides DNS.")

The whole thing is one Python file with no dependencies beyond the Azure CLI. It needs only Reader access, and it changes nothing.

Run it in a pull request

Because it is read-only and fast, it works well as a gate. A sample GitHub Actions job using federated credentials (no stored secrets):

yaml
name: private-path-check
on: pull_request

permissions:
id-token: write
contents: read

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

  - uses: azure/login@v2
    with:
      client-id: ${{ vars.AZURE_CLIENT_ID }}
      tenant-id: ${{ vars.AZURE_TENANT_ID }}
      subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}

  - name: Verify private path
    run: |
      python private_path_check.py \
        --resource-id "${{ vars.TARGET_RESOURCE_ID }}" \
        --source-vnet-id "${{ vars.SOURCE_VNET_ID }}"
Enter fullscreen mode Exit fullscreen mode

Run it after the infrastructure is deployed to Dev, and a missing VNet link fails the pipeline in a minute, not after a day of debugging.

Why this matters more with AI-generated infrastructure

If an AI assistant writes your Bicep or Terraform, it will happily produce a private endpoint and forget the DNS zone link. The code is valid, what-if is happy, and the failure only appears at runtime. Checks like this one are the kind of gate that makes AI-generated changes safe to review: the model proposes, and an independent check verifies the result.

What it does not do

I want to be clear about the limits:

It does not test the network itself. NSG rules, firewall rules, routing, and open ports are not checked. For that, use Azure Network Watcher (connection troubleshoot and the virtual network verifier).
Hub-and-spoke setups need care. If DNS resolution runs through a hub VNet or a Private Resolver, pass the hub VNet as the source, or the "linked" check will report a false failure.
The zone table is small. It covers storage, Key Vault, SQL, and Databricks. Other services need an entry from the Microsoft Learn list of private endpoint DNS zone values.
The A record name is not always the resource name. That holds for storage, but some services register different names.
It reads configuration. It does not resolve DNS from inside the VNet. A real nslookup from a VM in the VNet is still the final proof.

Top comments (2)

Collapse
 
m_montazeri profile image
Mohammad Montazeri •

Two small refinements could make this preflight sharper. For custom/hybrid DNS, distinguish the private zone stored in Azure from the public service zone used for conditional forwarding. Microsoft’s zone table lists privatelink.dfs.core.windows.net as the private zone and dfs.core.windows.net as its public DNS zone forwarder. Forwarding only the former is not the general configuration described there.

For the final lookup, I would compare the answer with the intended private endpoint’s actual IP, rather than treating any 10.x answer as success. That also catches a stale record or a different private endpoint.

AI-assisted reply; I work on Lisar Connect.

Collapse
 
manojkagitha profile image
Manoj Kumar Kagitha •

Number 3 is the trap that gets everyone. The zone exists, the endpoint is approved, the A record is there, and the VNet doing the lookup cannot see the zone.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.