Agent Foskett Case File • Azure Networking • Private Link • Storage Security • KQL

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.” ☕😂

Agent Foskett investigates Azure Private Link and an exposed public storage endpoint
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.

✓ Prove private DNS and connectivity separately
✓ Check publicNetworkAccess and networkAcls
✓ Correlate changes with actual access evidence

The network test passed — and missed the exposure

At 08:40 UTC, an engineer validated a new Azure Storage private endpoint from a virtual machine in the application subnet. DNS resolved 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.
08:40 UTCPrivate endpoint DNS and authenticated read pass.
11:17 UTCReview finds public network access still enabled.
12:40 UTCPublic access disabled; internal and external tests repeated.

Private Link changes a route — not automatically the public policy

A private endpoint provides a private IP in a virtual network for a supported Azure service. Private DNS helps clients inside that network choose the private route. It does not, by itself, disable the service's public network endpoint.

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 approved design called for application traffic over Private Link and no public data-plane connectivity. The infrastructure deployment created the private endpoint, its DNS zone group and the zone link, but the storage account's public network setting was not updated. The release checklist tested the private path and never asserted the public-access policy.

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

Record the storage account resource ID, subscription, region, resource provider API version, 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

In Microsoft Sentinel or Log Analytics, 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.
storage-account-configuration-changes.kql
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 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 asc

KQL: Inspect public-endpoint blob requests

Where Azure Storage diagnostic settings send Blob service resource logs to a workspace using the resource-specific table, 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.
storage-blob-public-path-review.kql
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 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 asc

KQL: Correlate storage security alerts

If Defender for Storage or another detection source sends relevant alerts into Sentinel's 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.
related-storage-security-alerts.kql
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 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 asc

The decisive evidence: two independent paths

The configuration snapshot showed 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

1. 08:40 — Private route verified: approved client resolves the blob endpoint privately and authenticates.

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

After checking application dependencies, set the storage account's 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

Private endpoint success proves a private path exists. It does not prove the public endpoint is disabled.

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

“Agent Foskett, the private endpoint was definitely working.”

“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!
Develop IT.. Protect IT.. GEMXIT.
GEMXIT PTY LTD | GEMXIT UK LTD
Talk to GEMXIT

The Private Endpoint Was Working — So Why Was the Service Still Public?

A fictional Agent Foskett investigation into indirect prompt injection against a tool-enabled AI agent reading SharePoint documents.

AI agent prompt injection and trust boundaries

Understand how untrusted retrieved content can attempt to redirect an agent and why independent tool authorisation matters.

KQL evidence and application traces

Use Microsoft 365 audit and security telemetry where available, and inspect application-level agent and tool traces to determine actual outcomes.