top of page
Search

Your Private Endpoints Aren't Doing What You Think They're Doing

  • Writer: Logan Hemphill
    Logan Hemphill
  • Apr 3
  • 6 min read

Your storage accounts, SQL databases, and Key Vaults are probably still accessible from the public internet right now. Your team deployed Private Endpoints six months ago and assumed the job was done. It wasn't.


I run into this constantly. An organization spends weeks rolling out Private Endpoints across their Azure environment. They update the architecture diagrams. The network team signs off. Someone presents it to leadership as "we've moved to private connectivity." And the whole time, every single one of those resources is still answering requests from the public internet because nobody disabled public network access.


Private Endpoints don't turn off the front door. They add a back door. That's the part most teams miss.


How Private Endpoints actually work


When you create a Private Endpoint for an Azure PaaS service, Azure provisions a network interface in your VNet with a private IP address. Traffic from your VNet to that resource now travels over the Microsoft backbone network instead of the public internet.


But here's what doesn't happen: the resource's public endpoint doesn't go away. It's still there. It still accepts connections. If your storage account was reachable at mystorageaccount.blob.core.windows.net from the public internet before you created the Private Endpoint, it's still reachable from the public internet after you create it.


Microsoft's own documentation says it plainly. Creating a private link does not automatically block connections on the public endpoint. You have to disable public network access as a separate, deliberate step.


This isn't a bug. It's by design. Microsoft built it this way so you can transition to private connectivity without breaking existing integrations that still use the public endpoint. The problem is that most teams never complete that transition. They stop after step one and move on to the next project.


The gap between expectation and reality


I audited an environment last quarter where the team had deployed 47 Private Endpoints across storage accounts, SQL databases, Key Vaults, and Cosmos DB instances. They'd done real work. DNS zones were configured correctly. The Private Endpoints were healthy and connected. Traffic from their VNets seemed to be flowing over private connectivity.


But 43 of those 47 resources still had public network access set to Enabled and the Exception turned on for "Allow Azure services and resources to access this server" with firewall rules that had overly broad IP ranges that included the entire public ip range (/24) of singular peoples home Ip address, which effectively meant public access for anyone.


The security team thought they were covered. Their architecture diagram showed private connectivity. Their compliance checklist had "Private Endpoints: Deployed" marked as complete. But the actual security posture was identical to what it was before the project started, because the public endpoints were still wide open.


Why this keeps happening


Three reasons.

First, the Azure portal doesn't warn you. When you create a Private Endpoint through the portal, there's no prompt that says "would you also like to disable public access?" You have to navigate to a different blade on the resource to flip that switch. The networking configuration and the Private Endpoint configuration are in separate places, which makes it easy to forget.


Second, teams worry about breaking things. Disabling public network access on a storage account means that any external integration, any third party tool, any legacy application that connects over the public endpoint will break immediately. That's a legitimate concern. But the right answer is to map those dependencies and plan the cutover, not to leave the public endpoint open indefinitely.


Third, most monitoring and compliance tooling checks for the existence of Private Endpoints, not for the absence of public access. Defender for Cloud will tell you that a resource should use private endpoints. Once you create one, the recommendation goes green. It doesn't check whether you also disabled public access. Your compliance dashboard looks clean while your actual exposure hasn't changed.


How to find the gap in your environment


Here's an Azure Resource Graph query you can run right now to find storage accounts that have Private Endpoints but still allow public network access:

resources
| where type == "microsoft.storage/storageaccounts"
| where isnotnull(properties.privateEndpointConnections)
    and array_length(properties.privateEndpointConnections) > 0
| where properties.publicNetworkAccess == "Enabled"
    or properties.networkAcls.defaultAction == "Allow"
| project name, resourceGroup, subscriptionId,
    publicAccess = properties.publicNetworkAccess,
    defaultAction = properties.networkAcls.defaultAction,
    privateEndpointCount = array_length(properties.privateEndpointConnections)

Run that in the Azure portal under Resource Graph Explorer. If it returns results, you've got resources that your team thinks are private but aren't.


Here's the same concept for Key Vaults:

resources
| where type == "microsoft.keyvault/vaults"
| where isnotnull(properties.privateEndpointConnections)
    and array_length(properties.privateEndpointConnections) > 0
| where properties.publicNetworkAccess == "Enabled"
    or properties.networkAcls.defaultAction == "Allow"
| project name, resourceGroup, subscriptionId,
    publicAccess = properties.publicNetworkAccess,
    defaultAction = properties.networkAcls.defaultAction

And for SQL Servers:

resources
| where type == "microsoft.sql/servers"
| where isnotnull(properties.privateEndpointConnections)
    and array_length(properties.privateEndpointConnections) > 0
| where properties.publicNetworkAccess == "Enabled"
| project name, resourceGroup, subscriptionId,
    publicAccess = properties.publicNetworkAccess

If any of these queries return resources, you've found the gap. Those resources have private connectivity configured but are still answering calls from the internet.


How to fix it properly


Don't just flip the switch on everything at once. That's how you cause an outage on a Tuesday afternoon and spend the next four hours in a war room.


Step 1: Inventory your public dependencies. For each resource that shows up in the queries above, figure out what's connecting to it over the public endpoint. Check diagnostic logs for connections from public IPs. For storage accounts, you can use this CLI command to see the current network configuration:

az storage account show \
  --name yourstorageaccount \
  --resource-group yourresourcegroup \
  --query "{publicNetworkAccess:publicNetworkAccess, defaultAction:networkRuleSet.defaultAction, ipRules:networkRuleSet.ipRules}" \
  --output table

Step 2: Move to "Selected networks" first. Instead of jumping straight to disabled, transition to "Selected networks" and add only the IP ranges that legitimately need public access. This gives you a controlled tightening without cutting off everything at once.


Step 3: Disable public access. Once you've confirmed all legitimate traffic is flowing through Private Endpoints or approved IP ranges, disable public network access entirely. For a storage account:

az storage account update \
  --name yourstorageaccount \
  --resource-group yourresourcegroup \
  --public-network-access Disabled

Step 4: Enforce with Azure Policy. Deploy a policy that prevents anyone from re enabling public access after you've locked it down. Microsoft provides built in policy definitions for this. "Storage accounts should disable public network access" (ID: b2982f36-99f2-4db5-8eff-283140c09693) can be assigned in audit mode first, then switched to deny once you've cleaned up existing resources.


The cost of getting this wrong


This isn't an academic risk. A storage account with public network access enabled and a misconfigured SAS token is a data breach. A SQL database with public access enabled and a weak password is a data breach. The Private Endpoint gives you a false sense of security that makes these scenarios more likely, not less, because your team stops thinking about public exposure once they see "Private Endpoint: Connected" on the resource.


The Private Endpoint itself costs about $7.30 per month per endpoint ($0.01/hour), plus data processing charges starting at $0.01/GB for traffic through the endpoint. At scale, 50 endpoints run you about $365/month before data charges. That's not the expensive part. The expensive part is the security review you have to do after discovering that your "private" resources have been publicly accessible for a year.


A pattern that actually works


The organizations that get this right treat Private Endpoints and public access as a single configuration change, not two separate steps. They deploy both in the same Bicep template or Terraform module:

resource storageAccount 'Microsoft.Storage/storageAccounts@2024-01-01' = {
  name: storageAccountName
  location: location
  kind: 'StorageV2'
  sku: {
    name: 'Standard_LRS'
  }
  properties: {
    // Disable public access at deployment time
    publicNetworkAccess: 'Disabled'
    networkAcls: {
      defaultAction: 'Deny'
      bypass: 'AzureServices'
    }
  }
}

resource privateEndpoint 'Microsoft.Network/privateEndpoints@2024-07-01' = {
  name: '${storageAccountName}-pe'
  location: location
  properties: {
    subnet: {
      id: subnetId
    }
    privateLinkServiceConnections: [
      {
        name: '${storageAccountName}-connection'
        properties: {
          privateLinkServiceId: storageAccount.id
          groupIds: [
            'blob'
          ]
        }
      }
    ]
  }
}

When public access is disabled from the start, there's no transition period where the resource is exposed. The Private Endpoint is the only way in from day one.

For existing resources, pair the Resource Graph queries from earlier with a scheduled automation that flags any resource where a Private Endpoint exists but public access is still enabled. Run it weekly. Assign an owner to the findings. Treat "Private Endpoint deployed but public access enabled" as a security finding, not a configuration preference.


What to do Monday morning


Run the Resource Graph queries above. If they return results, you've got a problem that your compliance dashboard isn't showing you. Map the dependencies, plan the cutover, and close the public endpoints. Your Private Endpoints are only half the job until you do.


References

If this sounds like your environment, I do free 30 minute Azure cost reviews. It's a conversation to see if there's a fit, not a sales pitch.

 
 
 
bottom of page