Home / Azure

AzureTroubleshootingAdvanced

App Service can't reach a private endpoint after VNet integration: DNS, routing and NSG checks

A web app with regional VNet integration still times out or resolves a public IP for a private SQL, Storage or Key Vault endpoint. Test from inside the app's sandbox, then check DNS, routing, NSGs and the target firewall.

Deepak Bhardwaj6 min read

Applies toAzure App Service and Azure Functions on Basic tier or higher with regional VNet integration, Azure Private Link, Azure Private DNS

Symptom

You've turned on regional VNet integration and created a private endpoint for a backend such as Azure SQL, Storage or Key Vault. The app still fails, with one of these:

  • The connection times out.
  • The backend returns 403 or "public network access is disabled".
  • It works from a VM in the VNet, but not from the app.

VNet integration only changes outbound traffic from the app. It's a separate feature from a private endpoint on the app itself, which controls inbound traffic.

Step 1: test from inside the app

A test from a VM proves little, because the VM may use different DNS and routes. Open Advanced Tools (Kudu) or the SSH console for the app.

On Windows App Service (Kudu's debug console):

nameresolver mydb.database.windows.net
tcpping mydb.database.windows.net:1433

On Linux App Service (SSH):

nslookup mydb.database.windows.net
curl -v --connect-timeout 5 telnet://mydb.database.windows.net:1433

Tools vary by image, and you may have to install dnsutils or similar. Read the results like this:

Name resolves to TCP test Look at
Public IP Any DNS (Step 2)
Private IP Times out Routing and NSG (Steps 3-4)
Private IP Connects, but app gets 403 or login failure Target firewall and identity (Step 5)
No answer Fails The DNS server configured for the VNet

Step 2: fix DNS

This is the most common cause. The app must resolve the public name, through its CNAME chain, to the private IP. For Azure SQL, the chain is mydb.database.windows.net → mydb.privatelink.database.windows.net → private IP.

Check the zone, the record and the link:

az network private-dns zone list -g rg-dns -o table

az network private-dns record-set a list -g rg-dns \
  -z privatelink.database.windows.net \
  --query "[].{name:name, ip:aRecords[0].ipv4Address}" -o table

az network private-dns link vnet list -g rg-dns \
  -z privatelink.database.windows.net \
  --query "[].{name:name, vnet:virtualNetwork.id, state:virtualNetworkLinkState}" -o table

Common faults:

Fault Fix
Zone isn't linked to the integration VNet Link it, or link it to the VNet that holds the DNS servers the app uses
Record is missing Add a DNS zone group to the private endpoint
VNet uses custom DNS that doesn't forward Forward the privatelink zones, or the parent zone, to 168.63.129.16, or use a DNS Private Resolver
Wrong zone name Each service has its own zone. Blob uses privatelink.blob.core.windows.net

Check which DNS servers the VNet uses:

az network vnet show -g rg-net -n vnet-app --query "dhcpOptions.dnsServers"

The app picks up its DNS from the integrated VNet. You can override this with the WEBSITE_DNS_SERVER app setting, or the newer site property, so check that nobody has set an old value:

az webapp config appsettings list -g rg-app -n app-prod \
  --query "[?starts_with(name, 'WEBSITE_DNS')]" -o table

For more on the zones, see private endpoint DNS.

Tip

Restart the app after changing DNS settings. The app caches DNS results, so a fix may not show right away.

Step 3: check integration and routing

Confirm the integration is active, and on the subnet you expect:

az webapp vnet-integration list -g rg-app -n app-prod -o table

Then check the subnet. The integration subnet must be delegated to Microsoft.Web/serverFarms, and it needs enough free addresses to scale:

az network vnet subnet show -g rg-net --vnet-name vnet-app -n snet-appsvc \
  --query "{prefix:addressPrefix, delegations:delegations[].serviceName, udr:routeTable.id, nsg:networkSecurityGroup.id}"

Private (RFC1918) traffic goes through the VNet by default. Whether other outbound traffic does depends on the VNet route all setting:

az webapp show -g rg-app -n app-prod --query "{vnetRouteAll:vnetRouteAllEnabled}"

Private endpoint traffic within the VNet, or a peered VNet, needs no extra route. Watch for these:

  • The endpoint is in a peered VNet. Check that the peering is Connected, and that it's in both directions.
  • A UDR sends 10.0.0.0/8 to a firewall. Traffic to the private endpoint now goes through the firewall, which must allow it and keep the return path. See traffic bypassing the firewall for the opposite problem.
  • A globally peered or cross-region path. Confirm that the route and any NVA handle it.

Step 4: check the NSGs

Two places can block this:

  • An NSG on the integration subnet, outbound.
  • An NSG on the private endpoint subnet, inbound, if private endpoint network policies are enabled there.
az network vnet subnet show -g rg-net --vnet-name vnet-app -n snet-pe \
  --query "{policies:privateEndpointNetworkPolicies, nsg:networkSecurityGroup.id}"

VNet flow logs can show the denied flow from the integration subnet's address range. You can't run Network Watcher tools from inside the App Service sandbox, so flow logs are your best evidence.

Step 5: check the target

If TCP connects, the network is fine, and it's now about the service:

# Is the endpoint connection approved?
az network private-endpoint show -g rg-net -n pe-sql \
  --query "{state:privateLinkServiceConnections[0].privateLinkServiceConnectionState.status, provisioning:provisioningState}"

# SQL: public access setting
az sql server show -g rg-data -n mydb --query publicNetworkAccess

# Storage: default action
az storage account show -g rg-data -n stdata01 --query "{public:publicNetworkAccess, default:networkRuleSet.defaultAction}"

A connection state of Pending means someone has to approve it. And a 403 with a private IP in use can also be a missing data role. See managed identity 403 and storage 403.

The Key Vault reference exception

Key Vault references are resolved by the platform, not your code, so they can fail even when your code could reach the vault. To send them through the VNet, set:

az webapp update -g rg-app -n app-prod --set vnetRouteAllEnabled=true

Also see Key Vault reference not resolved. Depending on your setup, the managed identity and keyVaultReferenceIdentity may also need checking.

Warning

Turning on route-all sends all outbound traffic through the VNet. If the subnet has no NAT gateway or firewall path to the internet, calls to public APIs will start to fail. Check the outbound path before changing it on production.

Prevent it next time

  • Test name resolution from the app's own console as part of each deployment.
  • Create private endpoints with a DNS zone group, so records are managed automatically.
  • Size the integration subnet for scale-out, and see subnet IP exhaustion.
  • Keep DNS zone links in infrastructure as code, and alert on changes. A deleted link causes this kind of outage.