Detection rules › Kusto

Tailscale Premium: Unexpected exit-node egress

Status
available
Severity
medium
Time window
1h
Group by
ExitDst, NodeId, Src, SrcNodeName, SrcOs, SrcUser
Source
github.com/Azure/Azure-Sentinel

Identifies when a node sends traffic via an exit node not used in the prior 7-day baseline. First-seen exit destinations from a node may indicate routing-policy drift, data exfiltration, or compromise.

MITRE ATT&CK coverage

TacticTechniques
Command & Control
Exfiltration

Rule body

id: c1d2e3f4-1a2b-3c4d-5e6f-7a8b9c0d1e2f
name: "Tailscale Premium: Unexpected exit-node egress"
description: Identifies when a node sends traffic via an exit node not used in the prior 7-day baseline. First-seen exit destinations from a node may indicate routing-policy drift, data exfiltration, or compromise.
description-detailed: |
  Identifies when a node sends traffic via an exit node not used in the prior 7-day baseline. First-seen exit destinations from a specific source may indicate unsanctioned egress, a compromised node pivoting, or a policy misconfiguration. Requires Tailscale Premium or Enterprise.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: TailscalePremiumCCF
    dataTypes:
      - Tailscale_Network_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
  - Exfiltration
relevantTechniques:
  - T1090
  - T1041
query: |
  let baseline = 7d;
  let recent = 1h;
  let recentEgress =
      Tailscale_Network_CL
      | where TimeGenerated > ago(recent)
      | where HasExitTraffic
      | mv-expand t = ExitTraffic
      | extend Src = tostring(t.src), ExitDst = tostring(t.dst), Bytes = tolong(t.txBytes) + tolong(t.rxBytes)
      | summarize FirstSeen = min(TimeGenerated), TotalBytes = sum(Bytes) by NodeId, SrcNodeName, SrcUser, SrcOs, Src, ExitDst;
  let baselineEgress =
      Tailscale_Network_CL
      | where TimeGenerated between (ago(baseline + recent) .. ago(recent))
      | where HasExitTraffic
      | mv-expand t = ExitTraffic
      | extend Src = tostring(t.src), ExitDst = tostring(t.dst)
      | distinct NodeId, Src, ExitDst;
  recentEgress
  | join kind=leftanti baselineEgress on NodeId, Src, ExitDst
  | extend TotalMB = round(TotalBytes / 1024.0 / 1024.0, 2)
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: SrcNodeName
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: SrcUser
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: Src
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
kind: Scheduled
version: 1.0.0

Stages and Predicates

Parameters

let baseline = 7d;
let recent = 1h;

let recentEgress is inlined into the numbered stages below.

Let binding: baselineEgress used in Stage 7

let baselineEgress = Tailscale_Network_CL
    | where TimeGenerated between (ago(baseline + recent) .. ago(recent))
    | where HasExitTraffic
    | mv-expand t = ExitTraffic
    | extend Src = tostring(t.src), ExitDst = tostring(t.dst)
    | distinct NodeId, Src, ExitDst;

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

Stage 1: source

Tailscale_Network_CL

Stage 2: where

| where TimeGenerated > ago(recent)

Stage 3: where

| where HasExitTraffic

Stage 4: mv-expand

| mv-expand t = ExitTraffic

Stage 5: extend

| extend Src = tostring(t.src), ExitDst = tostring(t.dst), Bytes = tolong(t.txBytes) + tolong(t.rxBytes)

Stage 6: summarize

| summarize FirstSeen = min(TimeGenerated), TotalBytes = sum(Bytes) by NodeId, SrcNodeName, SrcUser, SrcOs, Src, ExitDst

Stage 7: join (negated)

recentEgress
| join kind=leftanti baselineEgress on NodeId, Src, ExitDst

Stage 8: extend

| extend TotalMB = round(TotalBytes / 1024.0 / 1024.0, 2)

Output fields

These fields are emitted when the rule matches.

FieldSource
ExitDstsummarize
FirstSeensummarize
NodeIdsummarize
Srcsummarize
SrcNodeNamesummarize
SrcOssummarize
SrcUsersummarize
TotalBytessummarize
TotalMBextend