Azure Networking

Service Endpoints Deep Dive: When Service Endpoint Policies Beat Private Endpoints

The industry consensus hardened a few years ago into a slogan: “Private Endpoints for everything, Service Endpoints are legacy.” It is a good default and a bad rule. A Service Endpoint is a route off your subnet plus a subnet identity the PaaS firewall recognises; a Private Endpoint is a private IP on a NIC inside your VNet that lets you switch the public endpoint off entirely. The Private Endpoint is unambiguously the stronger isolation primitive, and for regulated data it is the right answer. But “stronger isolation” is not the only axis a real design optimises. There is a third tool most teams never reach for — the Service Endpoint Policy — and once you understand it, a specific class of workloads stops being a Private Endpoint problem and becomes a much cheaper, much simpler one.

This article is the deep dive on Service Endpoints and, in particular, on Service Endpoint Policies: what the policy object is (a subnet-attached allow-list of exactly which Azure resources the Service Endpoint route may reach), how its alias model maps /services/Azure/storage onto real accounts, which services it covers today (Storage, with a regional-versus-global nuance that trips people), and the precise reasoning for when Service Endpoints plus a policy beat a Private Endpoint — large fleets of subnets, no-DNS environments, private-IP exhaustion, internal-only PaaS, high-throughput egress. It is the companion to Azure Private Endpoint vs Service Endpoint: Secure PaaS Access, which makes the broad choice; here we go to the bottom of the Service Endpoint half so the “exception” in that article is one you can build and defend. By the end you will know that a plain Service Endpoint is not an exfiltration control — it routes your subnet to the entire Storage service, an attacker’s account included — that a Service Endpoint Policy closes that gap with no private IP, DNS zone, or per-sub-resource NIC, and exactly where its hard limits (Storage-only; in-region by default; never “no public endpoint”) send you to a Private Endpoint instead.

What problem this solves

The default posture of Azure PaaS is a public endpoint: mystorage.blob.core.windows.net resolves to a Microsoft-owned public IP, reachable from anywhere, gated only by the service’s auth and an optional firewall. Two distinct risks fall out of that default, and people routinely conflate them. The first is inbound attack surface — the FQDN answers from the internet, so a leaked SAS or account key is a foothold. The second is outbound data exfiltration — a compromised workload or insider in your subnet writing your data out to a storage account in their subscription. Encryption does nothing for either; a “selected networks” firewall rule addresses the first but not the second.

A plain Service Endpoint makes the second risk worse in a way most engineers never notice. Turning on Microsoft.Storage pins the route and stamps the subnet identity — good — but it routes that subnet to all of Storage, every account in every tenant on the backbone; nothing stops the subnet from authenticating to a foreign account it should never touch. This is the gap a penetration tester loves.

What breaks without the right tool: a reviewer flags internal data services as exfiltration-reachable; a platform team rolls Private Endpoints to hundreds of subnets to close a gap that, for non-regulated data, never needed a private IP — burning address space, DNS zones, per-endpoint-hours and a NIC per Storage face; or a team without VNet-integrated DNS can’t make Private Endpoints resolve and quietly ships over the public endpoint anyway. The right tool for the internal-only, exfiltration-bounded, cost-sensitive case is a Service Endpoint Policy — and almost nobody knows it exists.

The field, framed before the deep dive — the three tools and the two risks, so you can see which tool closes which gap:

Mechanism Closes inbound surface? Closes outbound exfiltration? Private IP / DNS work? Cost
Public endpoint + firewall (“selected networks”) Narrows, never closes No — subnet still reaches all accounts None Free
Service Endpoint (route + identity) Narrows firewall only No — routes to the whole service None Free
Service Endpoint + Service Endpoint Policy Narrows firewall only Yes — allow-list of specific accounts None Free
Private Endpoint (private IP + Private Link) Yes — public can be Disabled Yes — targets one resource Private IP + Private DNS Hourly + per-GB

The row that does not exist in most people’s mental model is the third one: exfiltration control with zero private-IP or DNS cost. That is the whole reason this article exists.

Learning objectives

By the end of this article you can:

Prerequisites & where this fits

You should already understand Azure VNet fundamentals — a virtual network is your private address space carved into subnets, with Network Security Groups (NSGs) filtering traffic and route tables (UDRs) steering it. If those are fuzzy, read Azure Virtual Network: Subnets, NSGs, and Routing and User-Defined Routes & Route Tables first. You should know that PaaS services have a service firewall (the “Networking” blade that allows selected networks/IPs), how to run az in Cloud Shell, and the basics of how a Storage account exposes separate sub-resources (blob, file, queue, table).

This article sits in the Networking & PaaS security track as the narrow companion to the broad decision guide. Read Azure Private Endpoint vs Service Endpoint: Secure PaaS Access for the high-level choice and Private Endpoint mechanics, and Azure Private Link & Private DNS for PaaS for the at-scale Private DNS architecture; here we go to the bottom of the Service Endpoint half — the policy object, the alias model, the cost/sprawl reasoning. It pairs with Azure Storage Account Fundamentals (the only service with policy support today) and Azure Network Security Groups: Rules & Priorities (service tags meeting Service Endpoints). When a path breaks, Troubleshooting Azure Storage: 403, Firewall, Private Endpoint, RBAC & SAS and Troubleshooting Azure VNet Connectivity are the playbooks.

Core concepts

Five mental models make every later decision obvious; hold them and the deep sections become bookkeeping.

A Service Endpoint is a route plus an identity — nothing is placed in your VNet. Enabling Microsoft.Storage on a subnet does exactly two things: it injects optimised system routes so traffic from that subnet to Storage goes over the Azure backbone instead of the internet, and it stamps packets with the subnet’s identity (VNet + subnet resource ID) that the PaaS firewall can match in an “allow subnet X” rule. The account keeps its public IP, the FQDN keeps resolving public, and not one byte of new infrastructure lands in your address space. You are pinning the route and narrowing who the still-public service trusts — not making it private.

A plain Service Endpoint routes you to the whole service, which is why it is not exfiltration control. The optimised route is for all of Storage, so a workload in the subnet can authenticate to any account on the backbone — an attacker’s tenant included — and the Service Endpoint carries it, fast and audited, straight out of your estate. It reduced inbound exposure to your account but did nothing about outbound reach. This is the single most misunderstood fact about Service Endpoints.

A Service Endpoint Policy is the allow-list that closes the outbound gap. It is a standalone Azure resource you attach to a subnet, containing policy definitions — each an alias (which service it governs, e.g. /services/Azure/storage) plus a list of serviceResources (the exact account / resource-group / subscription IDs the subnet may reach). On-list traffic is allowed; anything off-list over that Service Endpoint is refused. The policy adds zero private IPs, zero DNS zones, and costs nothing — pure egress allow-listing on the backbone route.

“Private” and “no public endpoint” are words you still cannot say. A Service Endpoint Policy bounds where the subnet may go; it does not change what the service exposes. The storage account still has a public IP, the FQDN still resolves public, and you cannot set publicNetworkAccess = Disabled and keep Service-Endpoint reachability. If your security baseline is literally “no data service may present a public endpoint,” only a Private Endpoint satisfies it. The policy is the right tool when the requirement is “bound exfiltration and pin the route,” not “eliminate the public surface.”

The choice is an optimisation, not a hierarchy. Private Endpoint wins on isolation; Service Endpoint + Policy wins on cost, simplicity and private-IP frugality. For regulated data the isolation axis dominates; for internal-only, high-fan-out workloads the cost and operational axes dominate and Service Endpoint + Policy is genuinely the better answer. Knowing which axis dominates is the entire skill.

The vocabulary in one table

Before the deep sections, pin every moving part. The glossary repeats these for lookup; this is the mental model side by side:

Concept One-line definition Where it lives Why it matters
Service Endpoint Backbone route + subnet identity for a PaaS family Subnet (serviceEndpoints) Pins route; narrows inbound; routes to whole service
Subnet identity VNet+subnet ID stamped on egress packets On the wire What the PaaS firewall matches to allow you
Service firewall “Selected networks” allow-list on the PaaS resource The PaaS resource Where you reference the subnet/VNet
Service Endpoint Policy Subnet-attached allow-list of reachable resources Standalone resource → subnet Closes the outbound exfiltration gap
Policy definition One alias + a list of serviceResources Inside the policy The actual allow-list entry
Alias Selects which service the definition governs In the definition /services/Azure/storage etc.
serviceResources The resource IDs the subnet may reach In the definition Account / RG / subscription scope
Microsoft.Storage (regional) SE for Storage in the same region Subnet Default; cross-region needs Global
Microsoft.Storage.Global SE for Storage across regions Subnet Reach Storage in another region
Private Endpoint Private IP on a NIC in your subnet Subnet (NIC) The isolation alternative (public can be off)

How a Service Endpoint actually works

You enable a Service Endpoint on the subnet, not on the PaaS resource — the subnet’s serviceEndpoints collection lists the service families that get the optimised route and identity stamp. Two configurations must line up for traffic to be admitted: the subnet must carry the Service Endpoint, and the PaaS firewall must reference that subnet (or its VNet). Enabling one without the other is the most common “I turned it on and it still 403s” cause — the route is pinned but the firewall never learned to trust the subnet.

Enable the Service Endpoint on the subnet:

# Add the Microsoft.Storage service endpoint to a subnet
az network vnet subnet update \
  --resource-group rg-data --vnet-name vnet-spoke \
  --name snet-workload \
  --service-endpoints Microsoft.Storage

Then narrow the storage account’s firewall to that subnet — the half people forget:

# Tell the storage account to trust traffic from that subnet
SUBNET_ID=$(az network vnet subnet show \
  --resource-group rg-data --vnet-name vnet-spoke --name snet-workload \
  --query id -o tsv)

az storage account network-rule add \
  --resource-group rg-data --account-name mystorageacct \
  --subnet "$SUBNET_ID"

# And set the default action to Deny so only allowed networks pass
az storage account update --resource-group rg-data --name mystorageacct \
  --default-action Deny

The same two-sided wiring in Bicep, so it lives in source control:

resource vnet 'Microsoft.Network/virtualNetworks@2023-11-01' = {
  name: 'vnet-spoke'
  location: location
  properties: {
    addressSpace: { addressPrefixes: [ '10.20.0.0/16' ] }
    subnets: [
      {
        name: 'snet-workload'
        properties: {
          addressPrefix: '10.20.1.0/24'
          serviceEndpoints: [
            { service: 'Microsoft.Storage' }   // backbone route + subnet identity
          ]
        }
      }
    ]
  }
}

resource sa 'Microsoft.Storage/storageAccounts@2023-05-01' = {
  name: 'mystorageacct'
  location: location
  sku: { name: 'Standard_LRS' }
  kind: 'StorageV2'
  properties: {
    networkAcls: {
      defaultAction: 'Deny'               // close the firewall…
      virtualNetworkRules: [
        { id: vnet.properties.subnets[0].id, action: 'Allow' }  // …then trust the subnet
      ]
    }
  }
}

What “enabled” looks like — and what it still doesn’t change

After this, confirm the subnet carries the endpoint and the firewall trusts it:

# The subnet should now list the service endpoint
az network vnet subnet show \
  --resource-group rg-data --vnet-name vnet-spoke --name snet-workload \
  --query "serviceEndpoints[].service" -o tsv          # → Microsoft.Storage

# The account's firewall should list the subnet rule and Deny by default
az storage account show --resource-group rg-data --name mystorageacct \
  --query "networkAcls.{default:defaultAction, vnets:virtualNetworkRules[].id}" -o json

What changed and what did not — the table that prevents the two classic misreads (“it’s private now”, “exfiltration is closed”):

After enabling a Service Endpoint… True? Why it matters
Traffic to Storage takes the Azure backbone, not the internet Yes Lower latency, no internet hop, audited path
The subnet’s identity is presented to the Storage firewall Yes Lets you allow “selected networks” by subnet
The storage account still has a public IP Yes The audit surface persists; not “private”
The FQDN still resolves to the public IP Yes No Private DNS needed — and none gained
You can set publicNetworkAccess = Disabled and keep SE No SE needs the public face; only PE survives that
The subnet can reach only your account now No It still routes to all of Storage
On-prem clients over VPN/ExpressRoute can use the SE No Service Endpoints are VNet-local

Those “No” rows are the brief for the rest of the article: the exfiltration “No” is fixed by a Service Endpoint Policy (next); the on-prem and public-surface "No"s are the structural ceiling that pushes regulated workloads to Private Endpoints.

Service Endpoint coverage across PaaS

Service Endpoints exist for many PaaS families, but Service Endpoint Policies (the allow-list) exist for far fewer — knowing the difference per service stops you promising an exfiltration guard the platform can’t give yet:

Service Service Endpoint value SE supported? SE Policy (allow-list) today?
Azure Storage (Blob/File/Queue/Table) Microsoft.Storage / Microsoft.Storage.Global Yes Yes (the one rich case)
Azure SQL Database Microsoft.Sql Yes No (use firewall + PE)
Azure Key Vault Microsoft.KeyVault Yes No
Cosmos DB Microsoft.AzureCosmosDB Yes No
Service Bus Microsoft.ServiceBus Yes No
Event Hubs Microsoft.EventHub Yes No
Azure Database for MySQL/PostgreSQL Microsoft.Sql (varies) Yes No
App Service Microsoft.Web Yes No
Azure Container Registry Microsoft.ContainerRegistry Limited No

The asymmetry is the headline: Service Endpoint Policies are, in practice, a Storage feature — but Storage is the most common exfiltration target (it is where data lands), so the policy is more useful than its narrow scope suggests. Don’t assume the same allow-list exists for SQL or Key Vault; for those, exfiltration control comes from the service firewall, a Private Endpoint, or an Azure Firewall FQDN rule.

Service Endpoint Policies, in depth

A Service Endpoint Policy is a first-class Azure resource (Microsoft.Network/serviceEndpointPolicies) you attach to a subnet to restrict which specific resources that subnet may reach over its Service Endpoint. Without it, Microsoft.Storage on a subnet is a route to all of Storage; with it, the subnet reaches only the accounts (or resource groups, or subscriptions) you enumerate, and everything else over that Service Endpoint is refused at the platform.

The object model: policy → definitions → alias → resources

A policy is a container of one or more policy definitions, each with two load-bearing parts:

The object model in one table, since the nesting is where people get lost:

Layer What it is Cardinality Example value
Service Endpoint Policy The attachable resource attached to a subnet sep-storage-allowlist
Policy definition One alias + a resource list 1…n per policy def-storage
Alias Which service the definition governs 1 per definition /services/Azure/storage
serviceResources The resource IDs allowed 1…n per definition account / RG / subscription ID
Subnet attachment Where the policy takes effect the subnet’s serviceEndpointPolicies snet-workload

Authoring a policy: account-level allow-list

The tightest, most defensible form lists exactly the storage accounts the subnet legitimately uses:

# 1) Create the Service Endpoint Policy with a Storage definition
az network service-endpoint policy create \
  --resource-group rg-data --name sep-storage-allowlist \
  --location centralindia

# 2) Add a policy definition: alias = Storage, with the allowed account(s)
az network service-endpoint policy-definition create \
  --resource-group rg-data --policy-name sep-storage-allowlist \
  --name def-storage \
  --service "Microsoft.Storage" \
  --service-resources \
    /subscriptions/<sub>/resourceGroups/rg-data/providers/Microsoft.Storage/storageAccounts/mystorageacct \
    /subscriptions/<sub>/resourceGroups/rg-data/providers/Microsoft.Storage/storageAccounts/mylogsacct

Then attach the policy to the subnet that already carries the Microsoft.Storage Service Endpoint:

# 3) Attach the policy to the subnet (alongside the existing service endpoint)
az network vnet subnet update \
  --resource-group rg-data --vnet-name vnet-spoke --name snet-workload \
  --service-endpoint-policy sep-storage-allowlist

The same in Bicep, where it belongs:

resource sep 'Microsoft.Network/serviceEndpointPolicies@2023-11-01' = {
  name: 'sep-storage-allowlist'
  location: location
  properties: {
    serviceEndpointPolicyDefinitions: [
      {
        name: 'def-storage'
        properties: {
          service: 'Microsoft.Storage'
          serviceResources: [
            saAllowed.id                       // a specific storage account
            resourceGroup().id                 // …or scope to the whole RG
          ]
        }
      }
    ]
  }
}

resource subnet 'Microsoft.Network/virtualNetworks/subnets@2023-11-01' = {
  name: '${vnet.name}/snet-workload'
  properties: {
    addressPrefix: '10.20.1.0/24'
    serviceEndpoints: [ { service: 'Microsoft.Storage' } ]
    serviceEndpointPolicies: [ { id: sep.id } ]   // attach the allow-list
  }
}

Scope choices: account vs resource group vs subscription

The serviceResources list accepts three granularities — a direct trade between operational toil and exfiltration tightness:

Scope in serviceResources What the subnet may reach Maintenance burden Exfiltration tightness
Specific storage account ID Only that account High (edit on every new account) Tightest — exactly the accounts you list
Resource group ID Every account in that RG Medium (new accounts in the RG auto-allowed) Medium — RG is the trust boundary
Subscription ID Every account in the subscription Low (set once) Loosest — any account in the sub

A sound pattern is to scope the policy to a dedicated “approved data” resource group holding only sanctioned accounts, with Azure Policy deny blocking rogue creation — low maintenance (new sanctioned accounts auto-allowed) with a real boundary (the RG is the control point). Reserve account-level lists for the highest-sensitivity subnets where every reachable account must be explicit.

What happens to traffic that isn’t on the list

With a policy attached, the platform evaluates outbound Storage traffic against the allow-list: a listed resource proceeds over the backbone, an unlisted account over that Service Endpoint is refused. The policy governs traffic that uses the Service Endpoint — it is an egress allow-list on the optimised route, not a general-purpose firewall. The behaviours to remember:

Situation Behaviour with the policy attached
Subnet → a listed account (via SE) Allowed — normal backbone path
Subnet → an unlisted account (via SE) Refused — not reachable over the SE
Subnet → a listed account, but its firewall denies the subnet Still 403 — the account firewall is a separate gate
You attach a policy but the subnet has no Microsoft.Storage SE Policy has nothing to govern; enable the SE too
Definition has the wrong alias / missing service Policy doesn’t apply to Storage traffic — your control is silently absent
You list your own account incorrectly (typo’d ID) You lock yourself out — the #1 self-inflicted policy bug

The regional vs global Storage nuance

Service Endpoints for Storage come in two flavours. The plain Microsoft.Storage is regional — it optimises the route to Storage in the VNet’s own region; to reach a different region over a Service Endpoint you use Microsoft.Storage.Global. This matters for policies: the policy only governs traffic on the SE path, so a cross-region account needs the Global endpoint for the traffic to be on that path at all.

Endpoint Reaches Storage in… When to use Policy interaction
Microsoft.Storage (regional) The VNet’s own region Same-region account (the common case) Policy governs same-region SE traffic
Microsoft.Storage.Global Any region Cross-region account access over SE Needed for the cross-region traffic to be on the SE path

The common trip-up: a workload in Central India reaching a South India account over only the regional endpoint doesn’t take the cross-region SE path, so the firewall rule and policy don’t behave as expected. If data and compute span regions, add the Global endpoint deliberately.

When Service Endpoints (with a policy) beat Private Endpoints

The honest default in a modern enterprise landing zone is Private Endpoint, and for regulated data it is not negotiable. But several real workload shapes invert the trade, and a Service Endpoint plus a policy is the better engineering answer — cheaper, simpler, sufficient. The decisive factors, side by side, let you score a workload rather than argue by slogan:

Factor Service Endpoint + Policy Private Endpoint Who wins
Cost Free (route + policy) Per-endpoint hourly + per-GB, ×sub-resources SE for cost
DNS work None (FQDN stays public) Private DNS zone per service, must be solved SE for simplicity
Private IPs consumed Zero One per endpoint per sub-resource SE for IP frugality
Inbound surface Public IP persists Public can be Disabled PE for isolation
Exfiltration control Yes (allow-list of accounts) Yes (targets one resource) Tie (both close it)
On-prem reach No (VNet-local) Yes (over VPN/ExpressRoute) PE for hybrid
Per-resource granularity Account allow-list One resource per endpoint PE finer, SE simpler
Fan-out to many subnets One policy, attach widely One endpoint set per consuming VNet SE for scale
Service coverage Storage only (for the policy) Most PaaS PE for breadth

Read the table as a scoring sheet: if the rows that matter for this workload cluster on the left, the cheaper tool is also the correct one. Five concrete workload shapes invert the default, each for a reason rooted in one of those rows:

Shape Why Private Endpoint hurts here Why SE + Policy fits Deciding row
High fan-out — 50 internal subnets needing bounded Storage Endpoints/DNS/IPs replicated per consuming VNet One policy (scoped to the sanctioned RG) attaches to all 50; adding a subnet is one line Fan-out
No VNet-integrated DNS — hub with public DNS only, no DNS ownership A PE that resolves public is worse than useless (silently public or fails) Service Endpoints touch no DNS — nothing to misconfigure DNS work
Private-IP exhaustion — many accounts, tight 10.x space 1 IP per endpoint per sub-resource (4 per account × N) Zero private IPs — the control is a route + policy, not a NIC Private IPs
Internal, lower-sensitivity data — exfil requirement, not “no public” Pays for isolation the requirement never asked for Bound the subnet to sanctioned accounts; requirement met exactly Inbound surface
High-throughput Storage egress — analytics/media/backups Per-GB data charge on terabytes is real money Backbone carries it with no per-GB charge; policy bounds reach Cost

The common thread: the data is internal/non-regulated and the requirement is bound exfiltration, not eliminate the public surface. Match the control to the stated requirement, not to the strongest available control. The decision distilled to a table you can apply in a design review:

If the workload is… And the requirement is… Choose
Regulated / sensitive data “No public endpoint” Private Endpoint
Reachable from on-premises Hybrid access to the PaaS Private Endpoint
Internal-only, many subnets Bound exfiltration, pin route, low cost SE + Policy
In a no-VNet-DNS environment Any private-ish posture without DNS risk SE + Policy
IP-constrained, many accounts Avoid private-IP sprawl SE + Policy
High-throughput Storage egress Bounded reach without per-GB cost SE + Policy
Non-Storage PaaS needing exfil control Allow-list reach Private Endpoint (no SE Policy for it)

The honest summary: Private Endpoint is the default and the only answer for regulated, hybrid, or “no-public-endpoint” workloads; Service Endpoint + Policy is the correct answer — not a compromise — for internal-only, cost/IP-sensitive, high-fan-out Storage where the requirement is bounded exfiltration, not zero public surface.

Service Endpoints, NSGs and routing

Service Endpoints interact with NSGs and routing in ways that surprise people, especially with an Azure Firewall in the path.

NSGs and service tags. A Service Endpoint changes routing and identity, not NSG evaluation. Your egress NSG still applies; to allow the traffic cleanly, use the service tag for the destination (e.g. Storage or Storage.CentralIndia) in an outbound rule rather than chasing IP ranges, which change. Service tags and Service Endpoints are complementary: the tag is what the NSG allows, the Service Endpoint is how the traffic routes and identifies itself. See Azure Network Security Groups: Rules & Priorities for the tag mechanics.

UDRs and the forced-tunnel surprise. When you enable a Service Endpoint, Azure injects an optimised system route for that service’s prefixes with the Service Endpoint as the next hop. This route can take precedence over a 0.0.0.0/0 UDR that you installed to force all egress through an Azure Firewall — so Storage traffic from that subnet may go straight to Storage over the backbone, bypassing the firewall you believed inspected everything. That is fine (often desirable) if you intended it, and a silent hole in your egress-inspection story if you didn’t. The interaction worth memorising:

You configured… With a Service Endpoint on the subnet Result
0.0.0.0/0 UDR → Azure Firewall No SE for Storage Storage egress goes through the firewall (inspected)
0.0.0.0/0 UDR → Azure Firewall SE for Microsoft.Storage Storage egress may bypass the firewall via the optimised route
No forced tunnel SE for Storage Storage egress takes the backbone SE route (intended)
Firewall FQDN allow-list for *.blob... SE bypassing the firewall The FQDN rule is not consulted for that traffic

If your egress-control design depends on all traffic traversing the firewall (FQDN allow-listing, logging, IDPS), a Service Endpoint on that subnet quietly carves out an exception. Either don’t enable it on firewalled subnets, or document that Storage egress is governed by the Service Endpoint Policy instead — when the SE bypasses the firewall, the policy is your exfiltration control for that traffic.

Architecture at a glance

Picture a single workload subnet in a spoke VNet with the Microsoft.Storage Service Endpoint enabled and a Service Endpoint Policy attached. A process there resolves mystorageacct.blob.core.windows.net — still the public IP (no Private DNS anywhere) — and sends its request, which leaves over the Azure backbone carrying the subnet’s identity. The Service Endpoint Policy checks the destination against its allow-list: the sanctioned mystorageacct is listed, so it proceeds; a reach to a foreign attacker.blob.core.windows.net over the same Service Endpoint is refused at the platform. The allowed request then meets the storage account’s own firewall, which admits it because a “selected networks” rule trusts the subnet. Two independent gates — the policy (where may the subnet go) and the firewall (which subnets do I admit) — must both pass.

The numbered badges mark the five decisive facts and failure points: (1) the persistent public IP (not private; public access can’t be disabled); (2) the exfiltration gap a plain Service Endpoint leaves and the policy closes; (3) the policy refusing the unlisted foreign account on the route; (4) the account firewall as a second, independent gate; (5) the forced-tunnel surprise where the optimised route bypasses an Azure Firewall — which is exactly why the policy becomes the control for that traffic. The legend narrates each as symptom, confirm, and fix.

Left-to-right architecture of a Service Endpoint Policy governing Storage egress from a workload subnet. A spoke VNet subnet (10.20.1.0/24) carries the Microsoft.Storage service endpoint and an attached Service Endpoint Policy; a workload resolves the storage FQDN to its still-public IP. Traffic leaves over the Azure backbone carrying the subnet identity. The Service Endpoint Policy evaluates the destination against an allow-list: the sanctioned storage account is permitted while a foreign attacker-controlled account is refused on the route. The permitted traffic then meets the storage account's own firewall, which admits the trusted subnet, and reaches the still-public storage account. Numbered badges mark the decisive facts and failure points: (1) the account keeps a public IP and the FQDN resolves public, so this is not private and public access cannot be disabled; (2) the exfiltration gap a plain service endpoint leaves, routing the subnet to all of Storage; (3) the policy refusing the unlisted foreign account on the backbone route; (4) the account firewall as a second independent gate that must trust the subnet; (5) the forced-tunnel surprise where the optimised service-endpoint route can bypass an Azure Firewall meant to inspect all egress. A legend narrates each badge as symptom, confirm command and fix.

Real-world scenario

Finlytics, a mid-size analytics SaaS in Central India, runs a multi-tenant data platform: an Azure Databricks workspace and a fleet of AKS batch jobs in fourteen spoke subnets, all staging large volumes of non-regulated event telemetry into a handful of Standard_LRS storage accounts in a rg-data-approved resource group. Their security baseline had one hard rule for this tier: the workload subnets must not be able to exfiltrate telemetry to any storage account the company does not own. Note what it did not say — it did not require “no public endpoint” for this non-regulated telemetry. Monthly spend was about ₹4.6 lakh.

The first design, dutifully following the “Private Endpoints for everything” slogan, put a Private Endpoint on each of the four sub-resources of every staging account, with Private DNS zones linked to all fourteen spokes via the hub. It worked, and it hurt in three ways: a per-GB data charge on terabytes of daily staging (~₹38,000/month, pure tax for isolation nobody required); Private DNS plumbing across fourteen spokes that became the team’s top break-glass ticket source (a new spoke onboarded without a zone link silently staged over the public path); and private-IP burn — four faces × a dozen accounts — crowding an already-tight 10.x space two acquisitions had to fit into.

A design review reframed it against the actual requirement: non-regulated data, no on-prem consumers, one hard control — “no exfiltration to foreign accounts.” That is the exact shape a Service Endpoint Policy fits. They enabled Microsoft.Storage on the fourteen subnets, authored one policy (alias /services/Azure/storage) scoped to the rg-data-approved resource group, and attached it to all fourteen — plus an Azure Policy deny so no account could be created there without review, making the RG a real trust boundary. The staging accounts’ firewalls were set to Deny with the fourteen subnets allowed.

Two things needed care during cutover. A forgotten 0.0.0.0/0 UDR on three subnets meant the new Service Endpoint route bypassed the Azure Firewall they used — fine functionally, but it moved the exfiltration control to the policy, which they documented so the next auditor wasn’t surprised. And one analytics job read a dataset from South India, which the regional endpoint didn’t cover, so they added Microsoft.Storage.Global to that subnet and the policy. The re-audit passed: a penetration tester confirmed the subnets could not write to a personal MSDN account, the per-GB charge vanished, the DNS tickets stopped, and IP pressure eased — spend on the tier’s network controls dropped about ₹41,000/month. The lesson, in their design guide: “no exfiltration” is a Service Endpoint Policy; “no public endpoint” is a Private Endpoint — don’t pay for the second when the requirement is the first.

Advantages and disadvantages

The explicit two-column trade-off, then the prose on when each side matters:

Service Endpoint + Service Endpoint Policy Private Endpoint
Advantages Free (route + policy, no per-GB, no hourly); zero DNS work (FQDN stays public); zero private IPs consumed; one policy attaches to many subnets (fan-out); exfiltration allow-list for Storage (account/RG/subscription scope); trivial to enable; uses service tags in NSGs cleanly True private IP in your VNet; public endpoint can be Disabled (the only path to “no public surface”); reachable from on-premises over VPN/ExpressRoute; inherent per-resource isolation; supported across most PaaS; the answer for regulated data
Disadvantages Service keeps a public IP (audit finding persists; not “private”); cannot disable public access; VNet-local — no on-prem reach; the policy allow-list is Storage-only today; coarse (subnet-level identity); can silently bypass a forced-tunnel firewall; cross-region needs the Global endpoint Costs per hour and per GB; you must solve DNS (the #1 real-world failure); one endpoint per sub-resource (Storage gets pricey and IP-hungry); consumes private IPs; cross-tenant needs approval; more moving parts to operate at scale

When each matters maps straight onto the decision table above: Service Endpoint + Policy is the sweet spot for internal-only, non-regulated, cost/IP/DNS-sensitive Storage at fan-out (dev/test estates, telemetry/log staging, high-throughput egress); Private Endpoint is mandatory the moment data is regulated, the baseline says “no public endpoints,” you need on-prem reach, or the service isn’t Storage. In an enterprise landing zone Private Endpoint is the default and Service Endpoint + Policy is the documented, justified exception — write the cost/IP/DNS justification down so the next reviewer doesn’t reflexively “upgrade” you back into the charges you deliberately avoided.

Hands-on lab

A free-tier-friendly walk-through: enable a Service Endpoint, lock a storage account to the subnet, then add a Service Endpoint Policy and prove it blocks a second (unlisted) account. You need an Azure subscription and Cloud Shell; the compute is a tiny B1s VM and the Storage is Standard_LRS, so teardown at the end keeps cost to a few rupees.

1. Set variables and a resource group.

RG=rg-se-lab; LOC=centralindia
ALLOWED=stallowed$RANDOM; BLOCKED=stblocked$RANDOM
az group create -n $RG -l $LOC -o none

2. Create a VNet with a workload subnet (no Service Endpoint yet).

az network vnet create -g $RG -n vnet-lab --address-prefix 10.50.0.0/16 \
  --subnet-name snet-workload --subnet-prefix 10.50.1.0/24 -o none

3. Create two storage accounts — one you’ll allow, one you’ll prove is blocked.

az storage account create -g $RG -n $ALLOWED -l $LOC --sku Standard_LRS -o none
az storage account create -g $RG -n $BLOCKED -l $LOC --sku Standard_LRS -o none

4. Enable the Service Endpoint on the subnet, and lock the ALLOWED account to it.

az network vnet subnet update -g $RG --vnet-name vnet-lab -n snet-workload \
  --service-endpoints Microsoft.Storage -o none

SUBNET_ID=$(az network vnet subnet show -g $RG --vnet-name vnet-lab -n snet-workload --query id -o tsv)

az storage account network-rule add -g $RG --account-name $ALLOWED --subnet "$SUBNET_ID" -o none
az storage account update -g $RG -n $ALLOWED --default-action Deny -o none

5. Author the Service Endpoint Policy allowing ONLY the ALLOWED account, and attach it.

ALLOWED_ID=$(az storage account show -g $RG -n $ALLOWED --query id -o tsv)

az network service-endpoint policy create -g $RG -n sep-lab -l $LOC -o none
az network service-endpoint policy-definition create -g $RG --policy-name sep-lab \
  -n def-storage --service Microsoft.Storage --service-resources "$ALLOWED_ID" -o none

az network vnet subnet update -g $RG --vnet-name vnet-lab -n snet-workload \
  --service-endpoint-policy sep-lab -o none

6. Deploy a tiny VM in the subnet to test from inside.

az vm create -g $RG -n vm-test --image Ubuntu2204 --size Standard_B1s \
  --vnet-name vnet-lab --subnet snet-workload --public-ip-address "" \
  --admin-username azureuser --generate-ssh-keys -o none

7. From the VM (via az vm run-command), prove the allow-list works. You’re verifying reachability, not auth — an anonymous blob request returns an auth error either way; the point is that the ALLOWED host answers while the BLOCKED host is not reachable over the Service Endpoint.

az vm run-command invoke -g $RG -n vm-test --command-id RunShellScript \
  --scripts "curl -s -o /dev/null -w 'ALLOWED:%{http_code}\n' https://$ALLOWED.blob.core.windows.net/ ; \
             curl -s -o /dev/null -w 'BLOCKED:%{http_code} (or timeout)\n' --max-time 10 https://$BLOCKED.blob.core.windows.net/" \
  --query "value[0].message" -o tsv

Expected: ALLOWED returns an HTTP status (e.g. 400/409 — it answered), while BLOCKED fails to complete over the Service Endpoint. That contrast is the policy working.

8. Confirm the configuration via control-plane.

az network vnet subnet show -g $RG --vnet-name vnet-lab -n snet-workload \
  --query "{se:serviceEndpoints[].service, policy:serviceEndpointPolicies[].id}" -o json
az network service-endpoint policy-definition list -g $RG --policy-name sep-lab \
  --query "[].{name:name, service:service, resources:serviceResources}" -o json

9. Teardown.

az group delete -n $RG --yes --no-wait

You have proven both halves: the Service Endpoint + firewall admit the listed account (ALLOWED works), and the policy refuses the unlisted one (BLOCKED is refused on the route) — with no private IP and no Private DNS.

Common mistakes & troubleshooting

The failure modes that actually bite, as a playbook — symptom, root cause, the exact command or portal path to confirm, and the fix:

# Symptom Root cause Confirm with Fix
1 Enabled the Service Endpoint, still 403 from the subnet Account firewall never told to trust the subnet az storage account show --query networkAcls (no vnet rule / default Allow) az storage account network-rule add --subnet <id>; set --default-action Deny
2 Policy attached, but you locked yourself out Your own account ID missing/typo’d in serviceResources az network service-endpoint policy-definition list — your account not listed Add the correct account/RG/subscription ID to the definition
3 Policy has no effect; foreign account still reachable Subnet has the policy but no Microsoft.Storage SE az network vnet subnet show --query serviceEndpoints is empty Enable the Service Endpoint on the subnet too
4 Policy has no effect; wrong service governed Definition alias/service not Microsoft.Storage Definition service field wrong Recreate the definition with --service Microsoft.Storage
5 Cross-region account unreachable over SE Only regional Microsoft.Storage enabled Subnet lacks Microsoft.Storage.Global Add Microsoft.Storage.Global to the subnet’s service endpoints
6 On-prem clients can’t reach Storage privately Service Endpoints are VNet-local Branch nslookup returns public; SE doesn’t apply to on-prem Use a Private Endpoint for on-prem reach; SE can’t do it
7 Egress-inspection firewall not seeing Storage traffic Optimised SE route bypasses the 0.0.0.0/0 UDR Route table shows SE next-hop winning; firewall logs lack the flows Accept + document (policy is the control), or remove the SE on firewalled subnets
8 “We disabled public access and it broke” SE needs the public face; publicNetworkAccess=Disabled kills it Account publicNetworkAccess is Disabled while relying on SE Re-enable public + SE, or switch to a Private Endpoint
9 Tried to add an SE Policy for SQL/Key Vault SE Policies are Storage-only today No --service option resolves for those Use the service firewall or a Private Endpoint for non-Storage
10 New account in the RG not reachable, account-scoped policy Policy lists specific accounts, not the RG New account ID absent from serviceResources Add the account, or scope the definition to the RG/subscription

Three of these account for most lost hours. Row 1 (still 403) is the two-sided-wiring trap: the Service Endpoint pins the route on the subnet, but the account firewall must separately reference the subnet and Deny by default — people do one half and stop. Row 2 (locked yourself out) is the allow-list catching you: omit or typo your own production account in serviceResources and the subnet that needs it is refused. Row 7 (firewall blind) is the forced-tunnel bypass: the optimised Service Endpoint route out-prioritises a 0.0.0.0/0-to-firewall UDR, so Storage egress skips inspection — make the policy the documented control or drop the Service Endpoint on that subnet. Each row’s Confirm column is the exact command to prove it.

Best practices

Security notes

Cost & sizing

The whole reason this article exists is that the cost shapes differ sharply, and the cheaper one is sometimes correct:

A rough monthly picture for a non-regulated, high-fan-out internal Storage workload (a dozen accounts, fourteen subnets, terabytes/day staging):

Approach Resource cost Per-GB cost Private IPs DNS overhead Fits which requirement
Service Endpoint + 1 Policy ~₹0 ₹0 0 None Bound exfiltration, internal-only
Private Endpoint (per face, per account) Hourly × ~48 endpoints Per-GB on all staging ~48 Zones (+ resolver for on-prem) No public endpoint / regulated / hybrid
SE + Policy for staging, PE for the one regulated account ~₹0 for staging + a few PE Per-GB only on the PE account A handful Minimal zones Mixed — match each to its requirement

The third row is usually the mature answer: Service Endpoint + Policy for the bulk internal/non-regulated Storage, and Private Endpoints reserved for the specific accounts that genuinely need “no public endpoint” or on-prem reach. You stop paying per-GB and per-IP for isolation the bulk of the data never required, and you spend the Private Endpoint budget exactly where the requirement is real. Finlytics’ ~₹41,000/month saving came entirely from this reframing — not from a discount, but from matching the control to the requirement.

Interview & exam questions

1. Does enabling a Service Endpoint make a storage account private? No. A Service Endpoint pins the route to the Azure backbone and stamps the subnet identity so the account’s firewall can trust the subnet, but the account keeps its public IP, the FQDN still resolves public, and you cannot disable public access while relying on the Service Endpoint. “Private” and “no public endpoint” are Private-Endpoint properties.

2. Why is a plain Service Endpoint not an exfiltration control? Because it routes the subnet to the entire Storage service over the backbone, so a process in that subnet can reach any storage account, including one in an attacker’s tenant. The Service Endpoint reduced inbound exposure to your account (firewall trusts the subnet) but did nothing to bound outbound reach. You need a Service Endpoint Policy for that.

3. What is a Service Endpoint Policy, precisely? A standalone Azure resource attached to a subnet containing one or more definitions; each definition has an alias (which service it governs, e.g. /services/Azure/storage) and a list of serviceResources (the specific accounts, resource groups, or subscriptions the subnet may reach via the Service Endpoint). Traffic to a listed resource is allowed; traffic to anything off the list over that Service Endpoint is refused.

4. Which services support Service Endpoint Policies today? In practice, Azure Storage. Many services support Service Endpoints, but the policy (the resource allow-list) is a Storage feature. For non-Storage exfiltration control you use the service firewall, a Private Endpoint, or an Azure Firewall FQDN rule.

5. Name three workload shapes where Service Endpoint + Policy beats a Private Endpoint. (a) High fan-out — many internal subnets needing bounded Storage access, where one policy attaches widely versus per-VNet endpoints and DNS. (b) No VNet-integrated DNS — Private Endpoints that can’t reliably resolve private are worse than a Service Endpoint that needs no DNS. © Private-IP exhaustion or high per-GB throughput — Service Endpoints consume zero IPs and have no per-GB charge. In all three the data is internal/non-regulated and the requirement is bounded exfiltration, not “no public endpoint.”

6. What two independent gates must both pass for a request to a listed account? The Service Endpoint Policy (does the subnet’s allow-list permit this account?) and the account’s own firewall (does the account’s “selected networks” rule trust this subnet?). A correctly-listed account whose firewall doesn’t yet trust the subnet still returns 403, and vice versa.

7. How can a Service Endpoint break an egress-inspection design? Enabling a Service Endpoint injects an optimised system route that can take precedence over a 0.0.0.0/0 UDR pointing at Azure Firewall, so Storage egress goes straight to the backbone and bypasses the firewall’s FQDN allow-list and IDPS. Either don’t enable the Service Endpoint on firewalled subnets, or make the Service Endpoint Policy the documented exfiltration control for that traffic.

8. What’s the difference between regional Microsoft.Storage and Microsoft.Storage.Global? The regional endpoint optimises the route to Storage in the VNet’s own region; Microsoft.Storage.Global extends Service Endpoint behaviour to Storage in other regions. If your compute and a target account are in different regions, you need the Global endpoint for the cross-region traffic to take the Service Endpoint path (and for the policy to govern it).

9. Can on-premises clients use a Service Endpoint to reach Storage privately? No. Service Endpoints are VNet-local — the route and identity apply only to traffic originating in the VNet’s subnets. On-prem clients over VPN/ExpressRoute cannot use them; reaching Storage privately from on-prem requires a Private Endpoint with DNS forwarding.

10. You attached a Service Endpoint Policy and your own app can’t reach its storage account. First thing to check? Whether your account’s resource ID is on the policy’s serviceResources allow-list (and exact). A policy is an allow-list, so omitting or typo’ing your own account locks you out. Either add the correct ID or scope the definition to the resource group/subscription that contains it.

These map to AZ-700 (Network Engineer)design and implement private access to Azure services (Service Endpoints, Service Endpoint Policies, Private Endpoints, Private Link) — and AZ-500 (Security Engineer)configure network security for PaaS and data-exfiltration prevention. The Storage-firewall and account-scoping angle touches AZ-104. A compact cert-mapping for revision:

Question theme Primary cert Exam objective area
SE vs PE, what each changes AZ-700 Private access to Azure services
Service Endpoint Policy / exfiltration AZ-500 / AZ-700 Network security for PaaS; data protection
Storage firewall + selected networks AZ-104 / AZ-500 Configure storage network access
Forced tunnel / firewall bypass AZ-700 Routing, secure connectivity
Cross-region Storage SE (Global) AZ-700 Implement service endpoints

Quick check

  1. Your subnet has Microsoft.Storage enabled and the account firewall set to Deny with the subnet allowed — but a process there can still write to a storage account in an unrelated subscription. Why, and what one object fixes it?
  2. True or false: adding a Service Endpoint Policy lets you set the storage account’s publicNetworkAccess = Disabled and stay reachable from the subnet.
  3. You scope a Service Endpoint Policy definition to a resource group instead of listing accounts. Name one advantage and one risk.
  4. A subnet has a 0.0.0.0/0 UDR pointing at Azure Firewall and a Storage Service Endpoint. Where does Storage egress actually go, and what becomes your exfiltration control?
  5. Name two workload shapes where Service Endpoint + Policy is the better choice than Private Endpoint, and the requirement they share.

Answers

  1. A plain Service Endpoint routes the subnet to all of Storage, so it can reach foreign accounts — the firewall only controlled inbound trust to your account. The object that fixes it is a Service Endpoint Policy attached to the subnet, allow-listing exactly the accounts (or RG/subscription) the subnet may reach.
  2. False. A Service Endpoint (with or without a policy) needs the account’s public face; setting publicNetworkAccess = Disabled breaks Service-Endpoint reachability. Disabling public access and staying reachable is a Private Endpoint property.
  3. Advantage: low maintenance — new sanctioned accounts created in the RG are automatically allowed without editing the policy. Risk: the control is only as tight as membership in the RG, so a rogue account created there is auto-trusted unless you lock creation down (e.g. with Azure Policy deny).
  4. The optimised Service Endpoint route can take precedence over the 0.0.0.0/0 UDR, so Storage egress goes straight to the backbone, bypassing the Azure Firewall. The Service Endpoint Policy then becomes your exfiltration control for that traffic — document it.
  5. Any two of: high fan-out of internal subnets; no VNet-integrated DNS; private-IP exhaustion; high per-GB Storage throughput; internal-only lower-sensitivity data. The shared requirement is bound exfiltration / pin the routenot “eliminate the public endpoint” (which would mandate a Private Endpoint).

Glossary

Next steps

You can now build and defend a Service Endpoint + Policy design and know exactly when to spend on a Private Endpoint instead. Build outward:

AzureService EndpointsService Endpoint PoliciesPrivate EndpointData ExfiltrationPaaS SecurityNetworkingVNet
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading