<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Abdul</title>
    <description>The latest articles on DEV Community by Abdul (@bicep_and_beyond_dev).</description>
    <link>https://dev.to/bicep_and_beyond_dev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4231491%2Fbd05ae86-ae04-4774-a5a7-06b6c120e023.png</url>
      <title>DEV Community: Abdul</title>
      <link>https://dev.to/bicep_and_beyond_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bicep_and_beyond_dev"/>
    <language>en</language>
    <item>
      <title>Azure Storage Private Endpoints with Bicep: The DNS Bits Everyone Gets Wrong</title>
      <dc:creator>Abdul</dc:creator>
      <pubDate>Sat, 07 Nov 2026 16:10:26 +0000</pubDate>
      <link>https://dev.to/bicep_and_beyond_dev/azure-storage-private-endpoints-with-bicep-the-dns-bits-everyone-gets-wrong-40eb</link>
      <guid>https://dev.to/bicep_and_beyond_dev/azure-storage-private-endpoints-with-bicep-the-dns-bits-everyone-gets-wrong-40eb</guid>
      <description>&lt;p&gt;You turn off public access on a storage account, add a private endpoint, deploy... and your app gets a &lt;strong&gt;403 "This request is not authorized to perform this operation"&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The networking is almost never the problem. It's DNS.&lt;/p&gt;

&lt;p&gt;This post walks through a Bicep pattern that works, explains why each piece is there, and covers the hybrid DNS gotchas that bite once on-premises networks get involved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it breaks
&lt;/h2&gt;

&lt;p&gt;When you create a private endpoint for &lt;code&gt;mystorage.blob.core.windows.net&lt;/code&gt;, Azure changes the public DNS record into a CNAME:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mystorage.blob.core.windows.net
  -&amp;gt; CNAME mystorage.privatelink.blob.core.windows.net
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your client can resolve &lt;code&gt;privatelink.blob.core.windows.net&lt;/code&gt; from a &lt;strong&gt;private DNS zone&lt;/strong&gt;, it gets the endpoint's private IP (say &lt;code&gt;10.10.0.4&lt;/code&gt;) and everything works.&lt;/p&gt;

&lt;p&gt;If it can't, the lookup falls through to public DNS, the client gets the account's &lt;strong&gt;public&lt;/strong&gt; IP, and because public network access is disabled, the request is refused.&lt;/p&gt;

&lt;p&gt;So the job is: make sure every client resolves the &lt;code&gt;privatelink&lt;/code&gt; name privately.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five pieces
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;A storage account with public network access disabled&lt;/li&gt;
&lt;li&gt;A private DNS zone named &lt;code&gt;privatelink.blob.&amp;lt;storage suffix&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A link from that zone to your VNet&lt;/li&gt;
&lt;li&gt;The private endpoint itself, targeting the &lt;code&gt;blob&lt;/code&gt; sub-resource&lt;/li&gt;
&lt;li&gt;A DNS zone group on the endpoint, so Azure manages the A record for you&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 5 is the one most examples skip. Without it you end up creating A records by hand, and they silently go stale if the endpoint's IP ever changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bicep
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Storage account, locked down
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight bicep"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt; &lt;span class="s1"&gt;'Microsoft.Storage/storageAccounts@2023-01-01'&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;storageName&lt;/span&gt;
  &lt;span class="py"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;location&lt;/span&gt;
  &lt;span class="py"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'StorageV2'&lt;/span&gt;
  &lt;span class="py"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Standard_LRS'&lt;/span&gt;
  &lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="py"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;minimumTlsVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'TLS1_2'&lt;/span&gt;
    &lt;span class="py"&gt;supportsHttpsTrafficOnly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
    &lt;span class="py"&gt;allowBlobPublicAccess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
    &lt;span class="py"&gt;allowSharedKeyAccess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
    &lt;span class="py"&gt;defaultToOAuthAuthentication&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
    &lt;span class="py"&gt;publicNetworkAccess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Disabled'&lt;/span&gt;
    &lt;span class="py"&gt;networkAcls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
      &lt;span class="py"&gt;defaultAction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Deny'&lt;/span&gt;
      &lt;span class="py"&gt;bypass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'AzureServices'&lt;/span&gt;
    &lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;publicNetworkAccess: 'Disabled'&lt;/code&gt; is stronger than firewall rules: nothing outside your network can connect, whatever the ACLs say. Turning off shared keys pushes everyone onto Entra ID and RBAC, which is logged and revocable.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The private DNS zone
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight bicep"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="n"&gt;blobDnsZone&lt;/span&gt; &lt;span class="s1"&gt;'Microsoft.Network/privateDnsZones@2020-06-01'&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'privatelink.blob.&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;suffixes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;
  &lt;span class="py"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'global'&lt;/span&gt;
&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using &lt;code&gt;environment().suffixes.storage&lt;/code&gt; instead of hard-coding &lt;code&gt;core.windows.net&lt;/code&gt; keeps the template working in sovereign clouds, and keeps the Bicep linter happy.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Link the zone to the VNet
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight bicep"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="n"&gt;blobDnsLink&lt;/span&gt; &lt;span class="s1"&gt;'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01'&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;blobDnsZone&lt;/span&gt;
  &lt;span class="py"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;vnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;-link'&lt;/span&gt;
  &lt;span class="py"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'global'&lt;/span&gt;
  &lt;span class="py"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;registrationEnabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
    &lt;span class="py"&gt;virtualNetwork&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
      &lt;span class="py"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;vnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
    &lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A private DNS zone does nothing until it's linked to the VNet your clients use for DNS.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The private endpoint
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight bicep"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="n"&gt;blobPe&lt;/span&gt; &lt;span class="s1"&gt;'Microsoft.Network/privateEndpoints@2023-05-01'&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'pe-&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;storageName&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;-blob'&lt;/span&gt;
  &lt;span class="py"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;location&lt;/span&gt;
  &lt;span class="py"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;subnet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
      &lt;span class="py"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;privateEndpointSubnetId&lt;/span&gt;
    &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="py"&gt;privateLinkServiceConnections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="pi"&gt;{&lt;/span&gt;
        &lt;span class="py"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'blob'&lt;/span&gt;
        &lt;span class="py"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
          &lt;span class="py"&gt;privateLinkServiceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
          &lt;span class="py"&gt;groupIds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'blob'&lt;/span&gt;
          &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. The DNS zone group (don't skip this)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight bicep"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="n"&gt;blobPeDns&lt;/span&gt; &lt;span class="s1"&gt;'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-05-01'&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;blobPe&lt;/span&gt;
  &lt;span class="py"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'default'&lt;/span&gt;
  &lt;span class="py"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;privateDnsZoneConfigs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="pi"&gt;{&lt;/span&gt;
        &lt;span class="py"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'blob'&lt;/span&gt;
        &lt;span class="py"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
          &lt;span class="py"&gt;privateDnsZoneId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;blobDnsZone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
        &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells Azure to create and maintain the A record in the zone for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other storage services need their own zones
&lt;/h2&gt;

&lt;p&gt;Each storage sub-resource has its own endpoint and its own zone. If you use more than blob, repeat steps 2 to 5 for each:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Sub-resource (&lt;code&gt;groupIds&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;Private DNS zone&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;blob&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;privatelink.blob.core.windows.net&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;file&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;privatelink.file.core.windows.net&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;queue&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;privatelink.queue.core.windows.net&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;table&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;privatelink.table.core.windows.net&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;dfs&lt;/code&gt; (Data Lake Gen2)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;privatelink.dfs.core.windows.net&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Hybrid DNS: the part that bites later
&lt;/h2&gt;

&lt;p&gt;Everything above works for resources inside Azure. On-premises clients are where it gets interesting. (Thanks to the r/AZURE folks who added these in the comments on my original post.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forward the public zone, not the privatelink one.&lt;/strong&gt; On your on-prem DNS servers, create the conditional forwarder for &lt;code&gt;blob.core.windows.net&lt;/code&gt;, not &lt;code&gt;privatelink.blob.core.windows.net&lt;/code&gt;. If you only forward the privatelink zone, an upstream recursive resolver can answer the initial public name before your server ever gets to forward the CNAME target.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forward to something inside Azure that can see the zone.&lt;/strong&gt; Azure DNS Private Resolver (inbound endpoint) is the clean option. If you already run domain controllers or DNS servers in Azure, they can do the job and save you the resolver's cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Link the zone to the resolver's VNet too.&lt;/strong&gt; The resolver can only answer from zones linked to the VNet it lives in. In a hub-and-spoke design, that usually means linking every privatelink zone to the hub.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consider fallback to internet.&lt;/strong&gt; If some clients need to reach storage accounts that &lt;em&gt;don't&lt;/em&gt; have private endpoints, enable the fallback on the VNet link so names missing from the private zone resolve publicly (this property needs a recent API version for &lt;code&gt;virtualNetworkLinks&lt;/code&gt;, such as &lt;code&gt;2024-06-01&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight bicep"&gt;&lt;code&gt;&lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;registrationEnabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
  &lt;span class="py"&gt;resolutionPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'NxDomainRedirect'&lt;/span&gt;
  &lt;span class="py"&gt;virtualNetwork&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;vnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
  &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  At landing-zone scale: let Azure Policy do it
&lt;/h2&gt;

&lt;p&gt;The Bicep above suits a single workload, or learning how the pieces fit together. Across many subscriptions, Microsoft's Cloud Adoption Framework recommends a different split:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The platform team creates every &lt;code&gt;privatelink.*&lt;/code&gt; zone &lt;strong&gt;once&lt;/strong&gt;, in a central connectivity subscription, and links them to the hub.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DeployIfNotExists&lt;/strong&gt; policies watch for new private endpoints and automatically add the &lt;code&gt;privateDnsZoneGroups&lt;/code&gt; pointing at the central zones.&lt;/li&gt;
&lt;li&gt;App teams deploy only the endpoint itself and never touch DNS.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Microsoft documents this in its guide &lt;a href="https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/private-link-and-dns-integration-at-scale" rel="noopener noreferrer"&gt;Private Link and DNS integration at scale&lt;/a&gt;. If you run a landing zone, it's the approach to use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't forget disaster recovery
&lt;/h2&gt;

&lt;p&gt;Private endpoints are tied to a region. If you fail storage over to a secondary region, your existing endpoints and DNS records won't follow it automatically. Plan for this before you need it: create endpoints in the secondary region ahead of time, or have a tested runbook to update DNS, and include it in your DR tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  "It works, but I can't see my data in the portal"
&lt;/h2&gt;

&lt;p&gt;That's expected. With public access disabled, the portal's storage browser and &lt;code&gt;az storage&lt;/code&gt; commands from your laptop get a 403 too. Work from a VM or service inside (or peered to) the VNet, or temporarily allow your IP while you need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;publicNetworkAccess: 'Disabled'&lt;/code&gt; on the account&lt;/li&gt;
&lt;li&gt;[ ] One private DNS zone per sub-resource you use&lt;/li&gt;
&lt;li&gt;[ ] Each zone linked to the VNet(s) your clients resolve from, including the resolver's&lt;/li&gt;
&lt;li&gt;[ ] Private endpoint per sub-resource&lt;/li&gt;
&lt;li&gt;[ ] DNS zone group on every endpoint&lt;/li&gt;
&lt;li&gt;[ ] On-prem conditional forwarders for the &lt;strong&gt;public&lt;/strong&gt; zone names&lt;/li&gt;
&lt;li&gt;[ ] RBAC data roles (e.g. Storage Blob Data Contributor) for your apps' managed identities&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;nslookup&lt;/code&gt; from each network segment (spoke, hub, on-prem) returns the private IP&lt;/li&gt;
&lt;li&gt;[ ] At scale: central zones plus DeployIfNotExists policies instead of per-app DNS&lt;/li&gt;
&lt;li&gt;[ ] A DR plan for endpoints and DNS in the secondary region&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Want the complete, deployable version?&lt;/strong&gt; The storage + private endpoint template from this post is free: &lt;a href="https://aabdansa.gumroad.com/l/free-azure-storage-private-endpoint" rel="noopener noreferrer"&gt;download it here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It's also part of the &lt;a href="https://aabdansa.gumroad.com/l/azure-bicep-starter-pack" rel="noopener noreferrer"&gt;Azure Bicep Starter Pack&lt;/a&gt;, with a passwordless Web App + Azure SQL setup, a hub-and-spoke network and a PDF guide. If you run a landing zone, the &lt;a href="https://aabdansa.gumroad.com/l/landing-zone-private-dns-pack" rel="noopener noreferrer"&gt;Landing Zone Private DNS Pack&lt;/a&gt; implements the central-zones + Azure Policy approach described above.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>bicep</category>
      <category>devops</category>
      <category>cloud</category>
    </item>
  </channel>
</rss>
