Goal: an App Service that talks to storage/Key Vault over private IPs, with no public data-plane exposure.
Order matters; do it in this sequence:
1. **VNet + subnets.** One subnet for private endpoints, one delegated to App Service (`Microsoft.Web/serverFarms`) for regional VNet integration. The integration subnet needs a /26 or larger and cannot host anything else.
2. **Private DNS zones.** `privatelink.blob.core.windows.net`, `privatelink.vaultcore.azure.net`, etc., each linked to the VNet. Without the zone link, the app resolves the public IP and the firewall blocks it.
3. **Private endpoints** for each backend (storage account, Key Vault). Approve them; check the private IP resolves from inside the VNet (`nslookup` from a test VM or the app's console).
4. **Lock the backends down.** Set storage/Key Vault firewall to deny public access. Do this AFTER the endpoints work, not before, or you lock yourself out mid-build.
5. **Regional VNet integration on the App Service.** Requires a Standard-or-better plan (Basic has no VNet integration). Point it at the delegated subnet.
6. **App settings.** `WEBSITE_DNS_SERVER` is legacy; modern integration uses the VNet DNS. Set `WEBSITE_VNET_ROUTE_ALL=1` so ALL outbound traffic (not just RFC1918) goes through the VNet - without it, calls to the private endpoints still work via DNS, but mixed traffic leaks.
Traps:
- Forgetting WEBSITE_VNET_ROUTE_ALL=1 and wondering why the Key Vault call times out while the storage call works.
- Creating the private endpoint in a different region than the VNet (must match).
- Testing from your laptop: your laptop is not in the VNet, so post-lockdown you cannot reach the backends directly. Test from the app's console or a jump VM.
Verify: from the App Service console, `nameresolver` (Windows) or `nslookup` shows the private IP for the vault/storage host, and a test call (list secrets/blobs) succeeds with the public firewall denying.