PRACTITIONER GUIDE | CLOUD SECURITY
Practitioner GuideUpdated 12 min read

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

10 min
processing interval for NSG flow logs with Traffic Analytics enabled -- raw flow logs are processed every 10 minutes into queryable records
Version 2
NSG flow log format (required for byte count data) is not enabled by default -- must be explicitly selected when enabling flow logging
AzureNetworkAnalytics_CL
is the Log Analytics table that stores Traffic Analytics processed flow data -- this is the table for KQL threat detection queries
Free
NSG flow log collection itself has no per-GB charge -- cost is only the storage (blob) and optionally Traffic Analytics ($0.10/GB processed)

SponsoredRetool

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.

Start building for free today

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.

Free daily briefing

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

  1. Microsoft: NSG flow logs overview
  2. Microsoft: Traffic Analytics overview
  3. Microsoft: Query NSG flow logs with Traffic Analytics

Free resources

25
Free download

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.

No spam. Unsubscribe anytime.

Free download

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.

No spam. Unsubscribe anytime.

Free newsletter

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.

Eric Bang
Author

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.

Black Hat Giveaway

Win a $2,495 Black Hat pass.

Full-access to Black Hat USA 2026 in Las Vegas. Subscribe free to enter.

Joins Decryption Digest daily briefing. Unsubscribe anytime.

Giveaway: Black Hat USA 2026 Full-Access Pass ($2,495 value)

Details →
Daily Briefing

Subscribe to enter the giveaway

Every subscriber is automatically entered. You also get daily threat intel every morning: zero-days, ransomware, and nation-state campaigns. Free. No spam.

Already subscribed? You're already entered.

Giveaway

Win a $2,495 Black Hat USA 2026 pass.