Locking down Azure Logic Apps Standard with private endpoints is a common enterprise requirement. It's also an easy way to break the app with an error message that points you in the wrong direction. This post walks through a real troubleshooting session, the root cause, and a checklist you can reuse.
The setup
The goal was a fully private Logic App Standard:
- Inbound: the Logic App is reachable only through a private endpoint, with public access disabled.
- Outbound: the Logic App reaches its storage account only through VNet integration and storage private endpoints, with storage public access disabled.
- Hosting: Workflow Standard (WS1) plan, Windows.
The symptom
As soon as public network access on the storage account was disabled, the Logic App broke:
- The Overview blade showed
System.Private.CoreLib: Access to the path 'C:\home\site\wwwroot\host.json' is denied. - Runtime version showed Error.
- Saving a workflow failed with
Encountered an error (ServiceUnavailable) from host runtime. - Nobody on the team could create or edit workflows.
"Access denied" suggests a permissions problem, but it isn't one. Turning storage public access back on made everything work instantly. That one test isolated the fault to the private network path between the app and its storage.
Why storage matters so much
Logic App Standard runs on the Azure Functions runtime, and it needs all four storage services just to start:
| Service | Used for |
|---|---|
| File | The C:\home content share (host.json, workflow definitions), mounted over SMB |
| Blob | Runtime artifacts and state |
| Queue | Trigger and job orchestration |
| Table | Run history, workflow state, locks |
If any of these can't be reached, the host doesn't start. With storage locked down, there's no public fallback.
Key point: a private endpoint is inbound only
A private endpoint on the Logic App controls who can call the app. It does nothing for traffic the app sends out. For the runtime to reach private storage you also need:
- VNet integration on a dedicated subnet delegated to
Microsoft.Web/serverFarms. - Route All, so outbound traffic goes through the VNet.
- Content storage over VNet, so the file share mounts through the VNet.
- Private endpoints for blob, file, queue and table, each with a DNS record.
Also note that an App Service plan can't have a private endpoint. Private endpoints go on the apps and on the storage account.
Step 1: Configure outbound routing
In the portal, go to Logic App → Settings → Networking → Virtual network integration and click the subnet link. Then:
- Application routing → Outbound internet traffic: on (Route All)
- Configuration routing → Content storage: on
If those checkboxes aren't shown, add these app settings instead:
WEBSITE_VNET_ROUTE_ALL = 1
WEBSITE_CONTENTOVERVNET = 1
Step 2: Check the storage prerequisites
- Storage account → Settings → Configuration → Allow storage account key access: Enabled. The content share mount uses the storage key. If Azure Policy disables shared key access, the mount fails no matter how good your networking is.
WEBSITE_RUN_FROM_PACKAGEisn't set. It makeswwwrootread-only.- The file share named in
WEBSITE_CONTENTSHAREexists.
Step 3: Test DNS from Kudu
Open Kudu from a jump box or over VPN, since the app is private. For regional hostnames the URL looks like this:
https://<app>-<hash>.scm.<region>-01.azurewebsites.net/DebugConsole
ping and nslookup are blocked in the App Service sandbox. Use the built-in tools instead:
nameresolver <storage>.blob.core.windows.net
nameresolver <storage>.file.core.windows.net
nameresolver <storage>.queue.core.windows.net
nameresolver <storage>.table.core.windows.net
This was the result for blob:
nameresolver <storage>.blob.core.windows.net
-> CNAME <storage>.privatelink.blob.core.windows.net
-> no Address returned
nameresolver <storage>.privatelink.blob.core.windows.net
-> Non-existent domain
That was the root cause. The privatelink.blob.core.windows.net zone was linked to the VNet but had no A record for the storage account. Because the zone is authoritative, it returned NXDOMAIN instead of falling back to the public IP, so the runtime couldn't resolve storage at all.
Step 4: Fix the private DNS records
For each storage private endpoint, go to Private endpoint → Settings → DNS configuration → Add configuration and select the privatelink.<service>.core.windows.net zone that's linked to your VNet. This creates the A record and keeps it managed.
Then check each zone:
- Recordsets contain your storage account, pointing to a private IP.
- Virtual network links include the VNet used for integration.
After the fix, all four services resolved to private IPs:
| Service | Private IP |
|---|---|
| blob | 10.0.1.4 |
| file | 10.0.1.5 |
| queue | 10.0.1.6 |
| table | 10.0.1.7 |
Step 5: Test connectivity
tcpping <storage>.blob.core.windows.net:443
tcpping <storage>.queue.core.windows.net:443
tcpping <storage>.table.core.windows.net:443
All three connected.
Watch out for this one: tcpping to file:445 from Kudu always fails with "An attempt was made to access a socket in a way forbidden by its access permissions" (Windows error 10013). This isn't your network. The App Service sandbox blocks outbound ports 445, 137, 138 and 139 for in-app processes, and the platform mounts the content share outside the sandbox. To test 445 on the network, run this from a VM instead:
Test-NetConnection 10.0.1.5 -Port 445
Step 6: Restart cleanly
Restart the Logic App. If Runtime version still shows Error, do a full Stop, wait 30 seconds, then Start. That forces the content share to remount with the corrected DNS. Once Runtime version shows ~4, set storage public access back to Disabled.
Lessons learned
- "Access denied" on
host.jsonusually means the content share isn't mounted. It's rarely a real permissions issue. - Check all four storage services, not just blob and file. Queue and table are just as critical.
- Watch for duplicate
privatelink.*zones in hub and spoke setups. The record has to be in the zone that's linked to the integration VNet. - If your VNet uses custom DNS, forward
privatelink.*to 168.63.129.16 or to an Azure Private DNS Resolver. - Plan for developer access. Designer edits in the portal go through ARM and work from anywhere. Run history inputs and outputs, HTTP trigger testing and Kudu need line of sight into the VNet, through a jump box, VPN or a self-hosted CI/CD agent.
- Use the right tools in Kudu:
nameresolverandtcpping, notnslookupandping.
Reusable checklist
- ☐ VNet integration on a dedicated, delegated subnet
- ☐ Route All and Content storage turned on
- ☐ Storage private endpoints for blob, file, queue and table, each with a DNS zone group
- ☐ Private DNS zones linked to the integration VNet, with A records present
- ☐
nameresolverreturns private IPs for all four services - ☐ Shared key access enabled; content share exists; no
WEBSITE_RUN_FROM_PACKAGE - ☐ Storage public access disabled, app restarted, Runtime version shows
~4
In private networking, when something fails, check DNS first.