Detection rules › Kusto

Tailscale: Device Tailscale SSH newly enabled

Status
available
Severity
medium
Time window
2d
Group by
DeviceId
Source
github.com/Azure/Azure-Sentinel

Identifies when Tailscale SSH is enabled on a device that previously did not have it. SSH provides authenticated shell access over the tailnet using Tailscale identity, broadening attack surface if unexpected. Verify and confirm the SSH ACL covers it.

MITRE ATT&CK coverage

TacticTechniques
Persistence
Lateral Movement

Rule body

id: f0a1b2c3-4567-8901-23de-f12345670041
name: "Tailscale: Device Tailscale SSH newly enabled"
description: |
  Identifies when Tailscale SSH is enabled on a device that previously did not have it. SSH provides authenticated shell access over the tailnet using Tailscale identity, broadening attack surface if unexpected. Verify and confirm the SSH ACL covers it.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: TailscaleCCF
    dataTypes:
      - Tailscale_Devices_CL
queryFrequency: 1h
queryPeriod: 2d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
  - LateralMovement
relevantTechniques:
  - T1021
  - T1098
query: |
  let recent =
      Tailscale_Devices_CL
      | where TimeGenerated > ago(1h)
      | summarize arg_max(TimeGenerated, *) by DeviceId
      | where SshEnabled == true
      | project DeviceId, DeviceName, Hostname, User, Os, ClientVersion, LastSeen;
  let prior =
      Tailscale_Devices_CL
      | where TimeGenerated between (ago(2d) .. ago(1h))
      | summarize arg_max(TimeGenerated, *) by DeviceId
      | where SshEnabled == true
      | distinct DeviceId;
  recent
  | join kind=leftanti prior on DeviceId
  | project TimeGenerated = now(), DeviceName, Hostname, User, Os, ClientVersion, LastSeen
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: Hostname
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: User
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: P1D
    matchingMethod: AllEntities
kind: Scheduled
version: 1.0.0

Stages and Predicates

let recent is inlined into the numbered stages below.

Let binding: prior used in Stage 6

let prior = Tailscale_Devices_CL
    | where TimeGenerated between (ago(2d) .. ago(1h))
    | summarize arg_max(TimeGenerated, *) by DeviceId
    | where SshEnabled == true
    | distinct DeviceId;

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

Stage 1: source

Tailscale_Devices_CL

Stage 2: where

| where TimeGenerated > ago(1h)

Stage 3: summarize

| summarize arg_max(TimeGenerated, *) by DeviceId

Stage 4: where

| where SshEnabled == true

Stage 5: project

| project DeviceId, DeviceName, Hostname, User, Os, ClientVersion, LastSeen

Stage 6: join (negated)

recent
| join kind=leftanti prior on DeviceId

Stage 7: project

| project TimeGenerated = now(), DeviceName, Hostname, User, Os, ClientVersion, LastSeen

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
SshEnabledeqtrueexcludes:SshEnabled field:"SshEnabled" value:"true"

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
ClientVersionproject
DeviceNameproject
Hostnameproject
LastSeenproject
Osproject
TimeGeneratedproject
Userproject