Detection rules › Kusto
Tailscale: Device Tailscale SSH newly enabled
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
| Tactic | Techniques |
|---|---|
| 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
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.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
SshEnabled | eq | true | excludes:SshEnabled field:"SshEnabled" value:"true" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
SshEnabled | eq |
| field:"SshEnabled" kind:eq value:"true" |
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ClientVersion | project |
DeviceName | project |
Hostname | project |
LastSeen | project |
Os | project |
TimeGenerated | project |
User | project |