Symptom
You built hub-and-spoke with a UDR 0.0.0.0/0 -> Azure Firewall (and spoke-to-spoke routes via the firewall). Everything is inspected except traffic to private endpoints:
- Firewall logs show nothing for SQL, Storage or Key Vault private endpoint connections.
- Or the reverse: after you "fix" routing, connections to the private endpoint start timing out.
Why it happens
When a private endpoint is created, Azure injects a /32 system route for its IP into the endpoint's VNet and every peered VNet, with next hop InterfaceEndpoint. Longest prefix wins, so a /32 beats your 10.0.0.0/8 -> firewall UDR. Traffic goes straight to the endpoint.
The private endpoint subnet also has a property, privateEndpointNetworkPolicies, which controls whether NSGs and UDRs apply to private endpoints:
| Value | NSG applies to PE | UDR with a broader prefix overrides the /32 |
|---|---|---|
Disabled (old default) | No | No |
NetworkSecurityGroupEnabled | Yes | No |
RouteTableEnabled | No | Yes |
Enabled | Yes | Yes |
Step 1: confirm what the client's NIC actually uses
az network nic show-effective-route-table -g rg-spoke1 -n vm-app01-nic -o table \
| grep -E "InterfaceEndpoint|VirtualAppliance"
If you see 10.20.4.5/32 InterfaceEndpoint (the PE IP), the client is going direct.
Check the endpoint subnet setting:
az network vnet subnet show -g rg-spoke2 --vnet-name vnet-spoke2 -n snet-pe \
--query privateEndpointNetworkPolicies -o tsv
Step 2: decide where inspection should happen
You have two common designs:
- Inspect on the client side: the client's subnet gets a UDR that sends PE traffic to the firewall. This requires network policies on the PE subnet (that is where the /32 is invalidated) and a UDR on the client subnet.
- Don't inspect PE traffic at all: rely on NSGs on the PE subnet plus the PaaS service's own firewall. Simpler, and fine for many teams. Document it as an explicit decision.
Step 3: enable network policies on the PE subnet
az network vnet subnet update -g rg-spoke2 --vnet-name vnet-spoke2 -n snet-pe \
--private-endpoint-network-policies Enabled
With RouteTableEnabled or Enabled, a UDR with a broader prefix (for example the PE subnet /24) now overrides the injected /32 in the VNets that carry the UDR.
Add the route on the client subnet's route table:
az network route-table route create -g rg-spoke1 --route-table-name rt-spoke1 \
-n to-pe-subnet --address-prefix 10.20.4.0/24 \
--next-hop-type VirtualAppliance --next-hop-ip-address 10.0.1.4
Warning
Change routing in a maintenance window. Moving PE traffic onto the firewall without the firewall rules in place drops every database and storage connection from that spoke.
Step 4: keep the return path symmetric
The private endpoint replies straight to the client IP. If the request went through the firewall but the reply doesn't, the firewall sees half a TCP session and drops it, and the client times out.
Fix it with one of:
- SNAT on the firewall for traffic to private endpoints, so replies come back to the firewall. With Azure Firewall, application rules SNAT by default. For network rules, configure the private IP ranges (
Private IP ranges (SNAT)) so the PE range is not treated as private, which forces SNAT. Check the current firewall policy docs for your SKU. - A UDR on the PE subnet pointing the client range back to the firewall. This only works if network policies are
RouteTableEnabled/Enabledthere.
SNAT is usually simpler. The trade-off is that the PaaS service (and its logs) sees the firewall's IP instead of the client's.
Step 5: test
# From the client VM: name should resolve to the PE IP, and the connection should succeed
nslookup sqlprod01.database.windows.net
nc -vz sqlprod01.database.windows.net 1433
Then confirm in the firewall logs:
AZFWNetworkRule
| where TimeGenerated > ago(30m)
| where DestinationIp == "10.20.4.5"
| project TimeGenerated, SourceIp, DestinationIp, DestinationPort, Action
Troubleshooting table
| Symptom | Cause | Fix |
|---|---|---|
| Firewall never sees PE traffic | /32 InterfaceEndpoint route wins | Enable network policies on the PE subnet and add a broader UDR on the client subnet |
| UDR added, still direct | UDR prefix is a /32 in a VNet where policies are off, or UDR is on the wrong subnet | Policies go on the PE subnet; the UDR goes on the client subnet |
| Connections time out after the change | Asymmetric return path | SNAT on the firewall, or a return UDR on the PE subnet |
| Works from spoke, fails from on-premises | Gateway subnet route table missing the PE prefix | Add the PE route to the GatewaySubnet route table |
| NSG on PE subnet ignored | Policies Disabled or RouteTableEnabled | Use NetworkSecurityGroupEnabled or Enabled |
Prevent it next time
- Create every private endpoint subnet with
--private-endpoint-network-policies Enabledfrom your landing zone templates. - Write down per spoke whether PE traffic is inspected or not, so nobody "fixes" one direction only.
- Keep DNS separate from routing: name resolution problems belong in private endpoint DNS, routing problems here.
- Related: Azure Firewall SNAT port exhaustion.