The ticket
CISO: “SQL is not on the internet.” The SQL blade shows Public network access = Selected networks, plus Allow Azure services on. A scanner in another tenant’s VM can still reach it from an Azure IP. You documented “private.” It is not.
Private Endpoint puts a NIC in your VNet (here the spoke 10.41.0.0/16) that maps to a PaaS resource (Storage, SQL, Key Vault, …). Traffic stays on the Microsoft backbone to that resource. Private Link is the platform that publishes the service to that NIC. Public + firewall IP list is still a public endpoint. “Allow trusted Azure services / Allow Azure services” is an exception for Azure-sourced IPs — not your VNet. Disable public access when the endpoint is live, and fix DNS with the privatelink.* zone so the FQDN returns the private IP.
Tenant techclick-lab.in · MG tc-root / tc-landing / tc-workloads · subs sub-hub sub-spoke-app · hub VNet 10.40.0.0/16 · spoke 10.41.0.0/16 · Azure Firewall 10.40.0.4 · VPN GW public 203.0.113.50 · on-prem FortiGate WAN 203.0.113.10. Not a live tenant.
Mental model
| Mode | Who can hit it | This lab |
|---|---|---|
| Public + any | The internet | Never for pay-api data |
| Public + IP list | Those IPs (and whoever shares them) | Break-glass only, if ever |
| Public + Allow Azure services | Azure-sourced IPs, not “our VNet” | Not private |
| Private Endpoint, public off | Your VNet / peered / on-prem via hub | pe-sql-pay in 10.41.0.0/16 |
If DNS still returns the public VIP, you built a museum piece. The app never uses the NIC.
How to choose
| Need | Do | Do not |
|---|---|---|
| PaaS from pay-api / on-prem | Private Endpoint + privatelink DNS | Allow Azure services |
| Partner on the internet | Their IP on the public firewall, or a different pattern | Pretend PE is optional |
| kv-tc-lab | PE in the spoke, public disabled | Portal-only “from my IP” forever |
Overview
Runbook
Side A — endpoint
Create PE in the spoke subnet (not GatewaySubnet, not AzureFirewallSubnet). Approve it. Repeat for
kv-tc-labif that vault is still public.Side B — DNS
Private DNS zone + VNet link on hub and spoke (and on-prem conditional forwarder to the hub if Priya must resolve it). FQDN must return 10.41.2.10 from pay-api.
Side C — prove
Disable public network access. From pay-api: resolve + connect. From a random Internet host: fail. From another Azure VM that is not peered: fail.
az network private-endpoint list -g rg-spoke -o table # Name PrivateIP Resource # pe-sql-pay 10.41.2.10 sql-pay # pe-kv-lab 10.41.2.11 kv-tc-lab # on pay-api nslookup sql-pay.database.windows.net # 10.41.2.10 # If you see a public A record, DNS is wrong — PE is unused.
Four failures
1 · Allow Azure services = private
It is not. It is “Azure-sourced.”
2 · PE without DNS
Clients still hit the public VIP. Public disable then looks like an outage.
3 · Public left On
You now have two doors. Attackers use the old one.
4 · PE in the hub, no route/DNS for the spoke
Works from a jump box in the hub. Dies on pay-api. Design DNS + UDR deliberately.
How to prove it
1) FQDN from pay-api = 10.41.x.x. 2) Public access is Disabled. 3) Allow Azure services is off. 4) A non-peered Azure VM cannot connect.
Traps
| Looks locked | Is not |
|---|---|
| Selected networks + Allow Azure | Other tenants’ Azure IPs |
| Private Endpoint created | Public still On + public DNS |
| Service endpoint on the subnet | Still a public PaaS VIP, just from that VNet — not Private Link |
Knowledge check
Judgment items. One best answer. Reasons send you back to the matching section.
Azure security class series: Shared + Entra · Landing zone · NSG vs Firewall vs WAF · Conditional Access · Key Vault + MI · Private Link · Defender + Sentinel · Hub-spoke · Identity path · Interview
Sources
Related: Azure session factory · Migration series.