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
403or "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/8to 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.