Azure NSG Flow Log Analysis for Threat Detection: How to Query Traffic Patterns That Reveal Lateral Movement, C2 Beaconing, and Data Exfiltration

Retool's new app builder is where AI-generated code ships safely
Building apps with AI is easy. Getting them to production safely is another story.
NSG flow logs are one of the most underutilized security data sources in Azure environments. Every denied connection is recorded, but so is every allowed connection -- including the lateral movement traffic that was allowed because the NSG rules were too permissive. Traffic Analytics processes the raw flow log data into a queryable format in Log Analytics, making it possible to run KQL queries that surface attacker behavior patterns. This guide covers enabling the data source correctly and the queries that detect the patterns most commonly seen in Azure security incidents.
Enabling NSG Flow Logs Version 2 with Traffic Analytics
NSG flow logs must be explicitly enabled per NSG. Version 1 logs capture connection tuples only. Version 2 adds bytes sent/received, which is required for exfiltration detection.
Enable via Azure CLI:
# Enable NSG flow logs V2 with Traffic Analytics
az network watcher flow-log create \
--location eastus \
--name nsg-flowlog-webservers \
--nsg "/subscriptions/sub-id/resourceGroups/rg-web/providers/Microsoft.Network/networkSecurityGroups/nsg-webservers" \
--storage-account "/subscriptions/sub-id/resourceGroups/rg-security/providers/Microsoft.Storage/storageAccounts/nsgflowlogs" \
--enabled true \
--format JSON \
--log-version 2 \
--retention 90 \
--traffic-analytics true \
--workspace "/subscriptions/sub-id/resourceGroups/rg-security/providers/Microsoft.OperationalInsights/workspaces/law-security" \
--interval 10
Verify flow logs are enabled and Traffic Analytics is running:
az network watcher flow-log list --location eastus --output table
For large environments with dozens of NSGs, use Azure Policy to audit which NSGs do not have flow logs enabled:
- Built-in policy:
Flow log should be enabled for every network security group(Policy ID: 27960feb-a23c-4577-8d36-ef8b5f35e0be)
After enabling, flow data appears in the AzureNetworkAnalytics_CL table in Log Analytics within 30-60 minutes.
Lateral Movement Detection: East-West Traffic Between Subnets
Lateral movement in Azure appears as unusual traffic between VMs that are in separate subnets or that have no legitimate reason to communicate.
// Detect VM-to-VM connections across subnet boundaries (potential lateral movement)
AzureNetworkAnalytics_CL
| where TimeGenerated > ago(24h)
| where FlowType_s == "AzurePublic" or FlowType_s == "IntraVNet"
| where L4Protocol_s == "T" // TCP
| extend SrcSubnet = strcat(split(SrcIP_s, ".")[0], ".", split(SrcIP_s, ".")[1], ".", split(SrcIP_s, ".")[2], ".0/24")
| extend DstSubnet = strcat(split(DestIP_s, ".")[0], ".", split(DestIP_s, ".")[1], ".", split(DestIP_s, ".")[2], ".0/24")
| where SrcSubnet != DstSubnet
| where DestPort_d in (22, 23, 135, 139, 445, 3389, 5985, 5986) // Admin/lateral movement ports
| summarize ConnectionCount = count(), DistinctDestinations = dcount(DestIP_s)
by SrcIP_s, SrcSubnet, DstSubnet, DestPort_d, FlowStatus_s
| where DistinctDestinations > 3 // Source hitting multiple destinations suggests scanning
| sort by ConnectionCount desc
A workstation-tier VM connecting to admin ports (445, 3389, 5985) on multiple servers in a different subnet is a high-priority lateral movement indicator. Cross-reference source IPs with your CMDB to identify the workload type.
Briefings like this, every morning before 9am.
Threat intel, active CVEs, and campaign alerts, distilled for practitioners. 50,000+ subscribers. No noise.
C2 Beaconing Detection: Periodic Small Outbound Connections
Command-and-control beaconing appears as periodic outbound connections from a compromised VM to an external IP, with regular intervals and small packet sizes.
// Detect periodic beaconing: consistent outbound connections to external IPs
let ExternalIPs = AzureNetworkAnalytics_CL
| where TimeGenerated > ago(24h)
| where FlowType_s == "ExternalPublic" and FlowDirection_s == "O"
| where FlowStatus_s == "A" // Allowed traffic only
| project TimeGenerated, SrcIP_s, DestIP_s, DestPort_d, BytesSentD_d;
ExternalIPs
| summarize
ConnectionCount = count(),
MinInterval = min(TimeGenerated),
MaxInterval = max(TimeGenerated),
AvgBytesPerFlow = avg(BytesSentD_d),
StdDevBytes = stdev(BytesSentD_d)
by SrcIP_s, DestIP_s, DestPort_d
| where ConnectionCount > 20 // High frequency
| where AvgBytesPerFlow < 5000 // Small packets (typical for beacons)
| where StdDevBytes < 500 // Low variance in packet size (regularity)
| sort by ConnectionCount desc
Connections with high frequency, low byte count, and low variance in byte size are the signature of C2 beaconing. The regularity (low StdDevBytes) distinguishes beaconing from legitimate but frequent API calls, which typically show higher byte size variance.
Port Scanning Detection and Data Exfiltration
Port scanning from an internal host:
// Detect internal host scanning multiple ports on multiple destinations (NSG denied traffic)
AzureNetworkAnalytics_CL
| where TimeGenerated > ago(1h)
| where FlowStatus_s == "D" // Denied connections only
| where FlowType_s == "IntraVNet" or FlowType_s == "AzurePublic"
| summarize
DistinctPorts = dcount(DestPort_d),
DistinctDests = dcount(DestIP_s),
TotalAttempts = count()
by SrcIP_s, bin(TimeGenerated, 5m)
| where DistinctPorts > 10 or DistinctDests > 20
| sort by TotalAttempts desc
Denied connection bursts from a single source to many ports or destinations in a short window indicate network scanning activity from a potentially compromised VM.
Large outbound data transfer (exfiltration):
// Detect unusually large outbound data transfers to external IPs
AzureNetworkAnalytics_CL
| where TimeGenerated > ago(24h)
| where FlowType_s == "ExternalPublic" and FlowDirection_s == "O"
| where FlowStatus_s == "A"
| summarize
TotalBytesSent = sum(BytesSentD_d),
TotalBytesReceived = sum(BytesReceivedD_d),
ConnectionCount = count()
by SrcIP_s, DestIP_s, DestPort_d
| where TotalBytesSent > 1000000000 // 1 GB outbound threshold
| sort by TotalBytesSent desc
For a baseline-adjusted version, compare against the rolling 7-day average for the same source IP:
let baseline = AzureNetworkAnalytics_CL
| where TimeGenerated between (ago(8d) .. ago(1d))
| where FlowType_s == "ExternalPublic" and FlowDirection_s == "O"
| summarize AvgDailyBytes = avg(BytesSentD_d) by SrcIP_s;
AzureNetworkAnalytics_CL
| where TimeGenerated > ago(1d)
| where FlowType_s == "ExternalPublic" and FlowDirection_s == "O"
| summarize DailyBytes = sum(BytesSentD_d) by SrcIP_s
| join kind=leftouter baseline on SrcIP_s
| extend Multiplier = DailyBytes / max_of(AvgDailyBytes, 1)
| where Multiplier > 10 // 10x above baseline
| sort by Multiplier desc
Building NSG Flow Log Alerts in Microsoft Sentinel
For continuous monitoring, create scheduled query rules in Sentinel based on the detection queries:
// Sentinel scheduled analytics rule: C2 beaconing candidate
AzureNetworkAnalytics_CL
| where TimeGenerated > ago(1h)
| where FlowType_s == "ExternalPublic" and FlowDirection_s == "O"
| where FlowStatus_s == "A"
| summarize
ConnectionCount = count(),
AvgBytes = avg(BytesSentD_d),
StdDevBytes = stdev(BytesSentD_d)
by SrcIP_s, DestIP_s, DestPort_d, bin(TimeGenerated, 5m)
| where ConnectionCount > 5
| where AvgBytes < 5000 and StdDevBytes < 200
| extend AlertDescription = strcat("Potential C2 beacon from ", SrcIP_s, " to ", DestIP_s, ":", tostring(DestPort_d))
Configure the rule to:
- Run every 5 minutes
- Look back 1 hour
- Threshold: trigger if query returns any results
- Severity: Medium (lateral movement queries: High)
- Map to MITRE ATT&CK: T1071 (Application Layer Protocol) for C2, T1046 (Network Service Discovery) for scanning
For alert enrichment, correlate the source VM IP with Azure resource data:
| join kind=leftouter (
AzureActivity
| where OperationNameValue == "MICROSOFT.COMPUTE/VIRTUALMACHINES/WRITE"
| summarize LastKnownName = arg_max(TimeGenerated, ResourceGroup) by Properties
) on $left.SrcIP_s == $right.Properties
The bottom line
NSG flow logs provide a complete record of network activity in Azure environments, but the data requires Traffic Analytics or SIEM ingestion to be actionable. Enable Version 2 flow logs on all NSGs with Traffic Analytics to populate the AzureNetworkAnalytics_CL table, run the lateral movement and beaconing queries on a scheduled basis, and configure Sentinel alerts on the highest-confidence patterns. Denied connection burst queries are especially effective for detecting scanning and reconnaissance from compromised VMs.
Frequently asked questions
How much storage do NSG flow logs consume?
Storage consumption depends heavily on traffic volume. For a medium-sized Azure environment (100-500 VMs), expect 20-50 GB per month of raw flow log data. Traffic Analytics processes this into a compressed, queryable format in Log Analytics at a cost of $0.10 per GB processed. The raw storage in Azure Blob is charged at standard LRS prices (approximately $0.018/GB/month). For most environments, the total monthly cost for NSG flow logs with Traffic Analytics is under $50.
Do NSG flow logs capture traffic within the same subnet?
No. NSG flow logs capture traffic that traverses the NSG. If two VMs are in the same subnet and communication between them does not traverse an NSG (which is the default for intra-subnet traffic), the flow is not logged. To capture intra-subnet traffic, attach a subnet-level NSG (which captures all traffic entering and leaving the subnet) or use Azure Virtual Network TAP for full packet capture. This is an important limitation for detecting lateral movement between VMs in the same subnet.
How do I correlate NSG flow log source IPs with Azure VM names?
Use Azure Resource Graph or the Azure Monitor query: `| join kind=leftouter (AzureNetworkAnalytics_CL | where SubType_s == 'VirtualMachine' | summarize by IPAddresses=split(PublicIPAddresses_s, '|'), Name=VMName_s) on $left.SrcIP_s == $right.IPAddresses`. Alternatively, maintain an IP-to-resource mapping by querying Azure Resource Graph for all network interfaces and their private IP assignments, and joining this to flow log queries on the source IP.
What is the difference between NSG flow logs and Azure Firewall logs?
NSG flow logs capture traffic at the virtual network level, recording allowed and denied flows at the granularity of NSG rules. Azure Firewall logs capture traffic at the application and network rule level, including FQDN-based logging and threat intelligence enrichment (known malicious IPs). Azure Firewall logs are more valuable for detecting C2 traffic to known-bad domains but only exist if an Azure Firewall is deployed. NSG flow logs are available in any environment using NSGs, which includes most Azure deployments. Use both together for comprehensive network visibility.
How do I use NSG flow logs to detect lateral movement between Azure VMs?
Query the flow logs in Log Analytics for east-west traffic patterns that indicate lateral movement: connections on common lateral movement ports (SMB port 445, WMI port 135, RDP port 3389, WinRM port 5985/5986) between VM subnets that should not need administrative communication. Build a baseline of expected administrative traffic by reviewing 30 days of flow logs and identifying which source-destination VM pairs regularly communicate on these ports. Alert on any new source-destination pair on these ports that was not present in the baseline. Also alert on SMB connections originating from non-management workstations to multiple distinct destinations within a short time window, which is a pattern consistent with automated lateral movement tools.
How do I configure NSG flow log retention and what is the cost impact?
NSG flow logs are stored in an Azure Storage Account you specify. Retention is configured on the storage account's blob lifecycle management policy, not within the NSG flow log setting itself. Set a lifecycle rule to delete blobs with the prefix 'insights-logs-networksecuritygroupflowevent/' after your desired retention period (90 days minimum for most compliance frameworks, 1 year for PCI DSS and HIPAA). Cost is primarily storage (approximately $0.02/GB/month for LRS) plus the Log Analytics ingestion cost if you forward logs to a workspace for querying. For high-traffic environments, flow log volume can be significant: use Traffic Analytics (built on top of NSG flow logs) to aggregate and sample the data, reducing Log Analytics ingestion costs while preserving query capability for threat hunting.
Sources & references
Free resources
Critical CVE Reference Card 2025–2026
25 actively exploited vulnerabilities with CVSS scores, exploit status, and patch availability. Print it, pin it, share it with your SOC team.
Ransomware Incident Response Playbook
Step-by-step 24-hour IR checklist covering detection, containment, eradication, and recovery. Built for SOC teams, IR leads, and CISOs.
Get threat intel before your inbox does.
50,000+ security professionals read Decryption Digest for early warnings on zero-days, ransomware, and nation-state campaigns. Free, daily, no spam.
Unsubscribe anytime. We never sell your data.

Founder & Cybersecurity Evangelist, Decryption Digest
Cybersecurity professional with expertise in threat intelligence, vulnerability research, and enterprise security. Covers zero-days, ransomware, and nation-state operations for 50,000+ security professionals every morning.
Win a $2,495 Black Hat pass.
Full-access to Black Hat USA 2026 in Las Vegas. Subscribe free to enter.
