Detection rules › Kusto

Tailscale Premium: Subnet router throughput anomaly

Status
available
Severity
low
Time window
8d
Group by
NodeId, SrcNodeName, SrcOs, SrcTags, SrcUser
Source
github.com/Azure/Azure-Sentinel

Identifies when a subnet router (gateway node bridging the tailnet to an on-prem or cloud subnet) handles 3x or more its 7-day baseline traffic in the last hour. Spikes can indicate exfiltration or scanning. Requires Tailscale Premium or Enterprise.

MITRE ATT&CK coverage

TacticTechniques
Command & Control
Exfiltration

Rule body

id: a5b6c7d8-5e6f-7a8b-9c0d-1e2f3a4b5c6d
name: "Tailscale Premium: Subnet router throughput anomaly"
description: |
  Identifies when a subnet router (gateway node bridging the tailnet to an on-prem or cloud subnet) handles 3x or more its 7-day baseline traffic in the last hour. Spikes can indicate exfiltration or scanning. Requires Tailscale Premium or Enterprise.
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: TailscalePremiumCCF
    dataTypes:
      - Tailscale_Network_CL
queryFrequency: 1h
queryPeriod: 8d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Exfiltration
  - CommandAndControl
relevantTechniques:
  - T1572
  - T1041
query: |
  let baselineDays = 7d;
  let recent = 1h;
  let multiplier = 3.0;
  let recentTraffic =
      Tailscale_Network_CL
      | where TimeGenerated > ago(recent)
      | where HasSubnetTraffic
      | mv-expand t = SubnetTraffic
      | extend Bytes = tolong(t.txBytes) + tolong(t.rxBytes)
      | summarize RecentBytes = sum(Bytes) by NodeId, SrcNodeName, SrcUser, SrcOs, SrcTags=tostring(SrcTags);
  let baseline =
      Tailscale_Network_CL
      | where TimeGenerated between (ago(baselineDays + recent) .. ago(recent))
      | where HasSubnetTraffic
      | mv-expand t = SubnetTraffic
      | extend Bytes = tolong(t.txBytes) + tolong(t.rxBytes)
      | summarize TotalBaselineBytes = sum(Bytes) by NodeId
      | extend BaselineHourlyBytes = TotalBaselineBytes / 168.0;
  recentTraffic
  | join kind=inner baseline on NodeId
  | where RecentBytes > BaselineHourlyBytes * multiplier
  | extend Multiplier = round(RecentBytes / BaselineHourlyBytes, 1), RecentMB = round(RecentBytes / 1024.0 / 1024.0, 2), BaselineHourlyMB = round(BaselineHourlyBytes / 1024.0 / 1024.0, 2)
  | project NodeId, SrcNodeName, SrcUser, SrcOs, SrcTags, RecentMB, BaselineHourlyMB, Multiplier
  | order by Multiplier desc
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: SrcNodeName
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: SrcUser
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: P1D
    matchingMethod: AllEntities
kind: Scheduled
version: 1.0.0

Stages and Predicates

Parameters

let baselineDays = 7d;
let recent = 1h;
let multiplier = 3.0;

let recentTraffic is inlined into the numbered stages below.

Let binding: baseline used in Stage 7

let baseline = Tailscale_Network_CL
    | where TimeGenerated between (ago(baselineDays + recent) .. ago(recent))
    | where HasSubnetTraffic
    | mv-expand t = SubnetTraffic
    | extend Bytes = tolong(t.txBytes) + tolong(t.rxBytes)
    | summarize TotalBaselineBytes = sum(Bytes) by NodeId
    | extend BaselineHourlyBytes = TotalBaselineBytes / 168.0;

Stages 1 to 6 define let recentTraffic (the rule's main pipeline source); stages 7 to 11 run on it.

Stage 1: source

Tailscale_Network_CL

Stage 2: where

| where TimeGenerated > ago(recent)

Stage 3: where

| where HasSubnetTraffic

Stage 4: mv-expand

| mv-expand t = SubnetTraffic

Stage 5: extend

| extend Bytes = tolong(t.txBytes) + tolong(t.rxBytes)

Stage 6: summarize

| summarize RecentBytes = sum(Bytes) by NodeId, SrcNodeName, SrcUser, SrcOs, SrcTags=tostring(SrcTags)

Stage 7: join

recentTraffic
| join kind=inner baseline on NodeId

Stage 8: where

| where RecentBytes > BaselineHourlyBytes * multiplier

Stage 9: extend

| extend Multiplier = round(RecentBytes / BaselineHourlyBytes, 1), RecentMB = round(RecentBytes / 1024.0 / 1024.0, 2), BaselineHourlyMB = round(BaselineHourlyBytes / 1024.0 / 1024.0, 2)

Stage 10: project

| project NodeId, SrcNodeName, SrcUser, SrcOs, SrcTags, RecentMB, BaselineHourlyMB, Multiplier

Stage 11: sort

| order by Multiplier desc

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
RecentBytescross_field_compare
  • BaselineHourlyBytes transforms: op:gt, rhs:mul:3.0
field:"RecentBytes" kind:cross_field_compare value:"BaselineHourlyBytes"

Output fields

These fields are emitted when the rule matches.

FieldSource
BaselineHourlyMBproject
Multiplierproject
NodeIdproject
RecentMBproject
SrcNodeNameproject
SrcOsproject
SrcTagsproject
SrcUserproject