The Private Endpoint Was Working — So Why Was the Service Still Public?
The private endpoint resolved to the correct private IP. The application worked. The deployment passed.
“Agent Foskett, the storage account is private.”
“Did you disable its public endpoint?”
“We created a private endpoint.”
“That answers a different question.” ☕😂

Case file: A private route was not a public-access restriction
A fictional Azure Storage incident where Private Link worked correctly while the storage account still allowed traffic through its public endpoint.
The network test passed — and missed the exposure
stcasefiles.blob.core.windows.net to a private address through the privatelink.blob.core.windows.net chain. A permitted blob request succeeded. The change ticket was marked complete.At 11:17 UTC, a separate security review found that
publicNetworkAccess was still Enabled and the storage account's network rule default action was Allow. An external test network could reach the public blob endpoint, subject to authentication and individual container permissions.This is a fictional training investigation. Names, addresses, timestamps and outcomes are illustrative. A public endpoint being reachable is not the same as anonymous blob access or proven data theft.
Private Link changes a route — not automatically the public policy
For this storage account, the private endpoint connection was approved and the private DNS zone was linked correctly. Those controls were functioning. The separate storage account property
publicNetworkAccess remained enabled; the network ACL default action allowed requests from networks not matched by more restrictive rules. The two facts can coexist.The investigator must distinguish network reachability, successful authentication, authorised data operations and anonymous access. Each requires different evidence.
The configuration drift that nobody checked
The issue was not a broken private endpoint. It was an incomplete security invariant: private endpoint approved AND public network access disabled. Investigators should inspect the actual deployed resource configuration, not infer it from a successful connection test or a green deployment status.
Preserve the evidence before changing the setting
publicNetworkAccess, networkAcls.defaultAction, IP and virtual network rules, private endpoint connection state, DNS zone links, role assignments and container-level anonymous access settings. Export relevant Activity Log events and any enabled blob data-plane logs.Collect both DNS results: a query from an intended private client and a query from an external network. DNS resolution alone is not proof of authorised data access. Capture the source IP, request IDs, HTTP status and authentication mode for any controlled external test; use a harmless test object and obtain approval before testing.
KQL: Find storage account configuration changes
AzureActivity can show control-plane operations when Azure Activity Logs are routed to the workspace. These events help establish who changed a resource and when; they do not reliably expose the complete final storage account configuration. Verify the current settings with Azure Resource Manager, Azure Resource Graph or the portal.- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
// Sentinel / Log Analytics: Azure Activity Logs must be connected
let storageId = tolower('/subscriptions/<subscription-id>/resourceGroups/<resource-group>/providers/Microsoft.Storage/storageAccounts/stcasefiles');
AzureActivity
| where TimeGenerated > ago(14d)
| where tolower(_ResourceId) == storageId
| where OperationNameValue has 'Microsoft.Storage/storageAccounts/write'
or OperationNameValue has 'Microsoft.Storage/storageAccounts/delete'
| project TimeGenerated, Caller, OperationNameValue,
ActivityStatusValue, CorrelationId, _ResourceId
| order by TimeGenerated ascKQL: Inspect public-endpoint blob requests
StorageBlobLogs can support investigation of successful and failed data-plane requests. The table is not populated automatically. Compare request caller IPs with approved egress addresses and private endpoint design; an external-looking IP or a successful request requires contextual verification before claiming exposure.- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
// Log Analytics: requires Blob diagnostic logs in StorageBlobLogs
// Replace with the real resource ID and expected private client addresses
let storageId = tolower('/subscriptions/<subscription-id>/resourceGroups/<resource-group>/providers/Microsoft.Storage/storageAccounts/stcasefiles');
StorageBlobLogs
| where TimeGenerated > ago(14d)
| where tolower(_ResourceId) == storageId
| project TimeGenerated, OperationName, StatusCode, CallerIpAddress,
AuthenticationType, Uri, RequesterObjectId, _ResourceId
| order by TimeGenerated ascKQL: Correlate storage security alerts
SecurityAlert table, search for signals around the affected account. Absence of alerts is not proof that public access was disabled, and an alert does not independently establish data exfiltration. Check alert ingestion, coverage and source records.- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
// Sentinel: requires relevant alerts ingested into SecurityAlert
SecurityAlert
| where TimeGenerated > ago(14d)
| where AlertName has_any ('Storage', 'Blob', 'Anonymous', 'Exfiltration')
or Description has 'stcasefiles'
| project TimeGenerated, AlertName, AlertSeverity,
ProviderName, Description, CompromisedEntity
| order by TimeGenerated ascThe decisive evidence: two independent paths
publicNetworkAccess = Enabled and networkAcls.defaultAction = Allow. From the application VNet, DNS resolved to a private address and an authenticated blob read succeeded. From an approved external test host, a request to the public endpoint also reached the storage service and returned a response consistent with the test identity's permissions.This proved that the private path worked and that the public path was not disabled. The team did not identify evidence of unauthorised blob reads in the retained logs. However, because blob diagnostic logging had been enabled only recently, the investigation could not rule out earlier activity solely from those logs. The final finding was confirmed unintended public network exposure, not confirmed data exfiltration.
The six-stage investigation timeline
2. 11:17 — Configuration discrepancy detected: public network access remains enabled and the default network rule allows traffic.
3. 11:28 — Scope established: record resource ID, affected services, containers, permissions and exposure window.
4. 11:46 — External test conducted: an authorised controlled request confirms the public path is reachable; no sensitive objects are used.
5. 12:05 — Activity reviewed: control-plane events and available blob logs are correlated, with logging gaps documented.
6. 12:40 — Policy corrected: public network access is disabled, private access retested, and an external negative test confirms the expected block.
Containment, remediation and verification
publicNetworkAccess to Disabled using the approved deployment process. Confirm all required clients have supported private connectivity and correct DNS; do not disable public access blindly where critical workloads still rely on it.Retest from the intended VNet and from an unauthorised external network. Review blob anonymous access settings, RBAC and SAS token exposure separately; disabling public networking is not a substitute for identity and authorisation controls. Add Azure Policy controls to audit or deny storage accounts with public network access enabled, enforce configuration-as-code checks, and alert on subsequent changes. Validate that diagnostic settings cover the required data-plane operations.
What Microsoft security teams should learn
DNS is not an access policy. Private DNS selects a route for configured clients; it does not secure every other client on the internet.
Check the deployed state. Inspect
publicNetworkAccess, firewall defaults, access controls and resource-specific settings.Keep claims proportional to evidence. Public reachability is an exposure; unauthorised data access requires additional proof.
Test both directions. A successful private test and a blocked external test belong in the same acceptance checklist.
Agent Foskett closes the case
“I know.”
“So why did security raise an incident?”
“Because the front door worked beautifully.”
“And?”
“You forgot to lock the back gate.”
He closed the Azure portal and picked up his coffee.
“Next time, test that the people who shouldn't get in actually can't.” ☕😂
The Logs Already Knew!

