Visual Studio Code Dev Tunnels Used as Remote Access

WindowsmacOSNetwork

TTP Hunt medium DPRK IT Worker FraudLaptop Farm

A developer laptop can be handed to a remote operator with no third-party software at all, using the tunnelling feature built into Visual Studio Code. ReliaQuest ranks it among the tools North Korean operators use most.

What this hunt looks for

VS Code's tunnelling feature publishes a machine to a Microsoft-hosted relay, reachable from a browser or another VS Code instance and authenticated by a GitHub or Microsoft account the employer does not control. It needs no installer, inbound firewall rule or unfamiliar binary, and the parent process is the editor the employee is meant to use.

ReliaQuest lists VS Code tunnelling among the most popular remote-access methods of North Korean IT workers, sometimes alongside an IP-KVM, and Unit 42's sweep covers it. This hunt looks for the tunnel subcommand on Windows and macOS, the code_tunnel.json file and macOS launch agent a tunnel leaves, and traffic to the Microsoft relay.

Technique
Remote Access Tools abuse through a legitimate developer tool (MITRE ATT&CK T1219) with command-and-control over a trusted third-party relay (T1102, T1572).
Data sources
EDR process telemetry, Proxy/DNS logs, macOS launchd configuration
Last updated
2026-10-04

Indicators

The atomic values this hunt matches on, listed separately from the queries so they can be lifted into tooling that speaks none of the query languages below. Every value here is reported by one of the sources cited at the foot of this page.

TypeValueNote
Command code tunnel Subcommand that publishes the host to the relay (Unit 42, ReliaQuest).
File path .vscode-cli/code_tunnel.json Created in the user's home profile when a VS Code tunnel is established, on Windows and macOS alike (MITRE ATT&CK, IDE Tunneling detection strategy).
File path (macOS) /Library/LaunchAgents/com.visualstudio.code.tunnel.plist Launch agent that code tunnel service install registers so the tunnel survives logout and reboot, in the system or a user LaunchAgents directory.
URL fragment vscode.dev/tunnel Browser entry point to a published tunnel (Unit 42).
Domain *.devtunnels.ms Microsoft dev tunnel service endpoints. Microsoft's VS Code documentation names only the relay control endpoint below, so verify this wildcard against your own proxy logs before relying on it.
Domain global.rel.tunnels.api.visualstudio.com Dev tunnel relay control endpoint, named in Microsoft's VS Code documentation as the domain to allow or deny to control the feature. The queries match the rel.tunnels.api.visualstudio.com suffix so that a prefix other than global still matches.

Logic

Every query below implements these steps, so the same hunt returns the same result in each product.

  1. Collect: process starts, file creations and relay lookups or connections on Windows and macOS endpoints
  2. Match: command line runs the tunnel subcommand (code or code-tunnel with 'tunnel', 'code tunnel', or vscode.dev/tunnel)
  3. Match: or code_tunnel.json is created under .vscode-cli, or the macOS launch agent com.visualstudio.code.tunnel.plist is written
  4. Match: or (optional) the relay (*.devtunnels.ms, rel.tunnels.api.visualstudio.com) is contacted on 3 or more days
  5. Report: devices with a match, newest first

Queries

Defender / Sentinel KQL

Written for this site, from indicators reported by ReliaQuest, Unit 42, MITRE ATT&CK

Rows are devices with a tunnel command or artifact; relay-3-days alone is a lead until a command or artifact backs it.

// Hunt: VS Code dev tunnel as remote access (Windows and macOS).
union
// optional: relay contact on 3 or more days; a developer debugging remotely does the same
( DeviceNetworkEvents
  | where Timestamp > ago(30d)
  | where RemoteUrl contains "devtunnels.ms" or RemoteUrl contains "rel.tunnels.api.visualstudio.com"
  | summarize Timestamp = max(Timestamp), ActiveDays = dcount(bin(Timestamp, 1d)) by DeviceName
  | where ActiveDays >= 3
  | project Timestamp, DeviceName, Signal = "relay-3-days", Evidence = tostring(ActiveDays) ),
( DeviceProcessEvents
  | where Timestamp > ago(30d)
  | where (FileName in~ ("code.exe", "code-tunnel.exe", "code", "code-tunnel") and ProcessCommandLine has "tunnel")
      or ProcessCommandLine contains "code tunnel"
      or ProcessCommandLine contains "vscode.dev/tunnel"
  | project Timestamp, DeviceName, Signal = "tunnel-command", Evidence = ProcessCommandLine ),
( DeviceFileEvents
  | where Timestamp > ago(30d) and ActionType in ("FileCreated", "FileModified", "FileRenamed")
  | where (FileName =~ "code_tunnel.json" and FolderPath contains ".vscode-cli")
      or (FileName =~ "com.visualstudio.code.tunnel.plist" and FolderPath contains "/Library/LaunchAgents")
  | project Timestamp, DeviceName, Signal = "tunnel-artifact-file", Evidence = strcat(FolderPath, "/", FileName) )
| summarize FirstSeen = min(Timestamp), LastSeen = max(Timestamp), Signals = make_set(Signal), Evidence = make_set(Evidence, 10) by DeviceName
| sort by LastSeen desc
Lower-fidelity parts (1)
  • the relay-3-days branch — a developer debugging remotely contacts the relay on many days too. Remove: delete from the // optional: relay contact line through the closing `),` of that branch

Sigma

Written for this site, from indicators reported by ReliaQuest, Unit 42

Single-event rule for the tunnel subcommand on Windows and macOS.

title: Visual Studio Code Dev Tunnel Published from an Endpoint
id: 99d9d20c-70aa-5a67-9d28-e7cb4804dfcb
status: experimental
description: >
  Detects the VS Code tunnel subcommand on Windows and macOS, which publishes
  the host to a Microsoft-hosted relay and allows it to be reached remotely
  with no third-party remote-access software installed. ReliaQuest ranks the
  technique among those most used by North Korean IT workers.
references:
  - https://reliaquest.com/blog/threat-spotlight-identifying-north-korean-insider-threats
  - https://unit42.paloaltonetworks.com/north-korean-it-workers/
  - https://attack.mitre.org/detectionstrategies/DET0133
logsource:
  category: process_creation
detection:
  selection_cli:
    Image|endswith:
      - '\code.exe'
      - '\code-tunnel.exe'
      - '/code'
      - '/code-tunnel'
    CommandLine|contains: 'tunnel'
  selection_any:
    CommandLine|contains:
      - 'code tunnel'
      - 'vscode.dev/tunnel'
  condition: 1 of selection_*
falsepositives:
  - Developers legitimately using dev tunnels for remote debugging or cross-device development, which is a supported Microsoft feature
level: medium

Splunk SPL

Written for this site, from indicators reported by ReliaQuest, Unit 42, MITRE ATT&CK

Device-level view across EDR and proxy data; the relay branch is the only one that needs the proxy index.

```Placeholders: index=edr is EDR process and file events; index=proxy is proxy or DNS logs with url and src_ip extracted.```
(index=edr process_name IN ("code.exe", "code-tunnel.exe", "code", "code-tunnel") process="*tunnel*")
    OR (index=edr (process="*code tunnel*" OR process="*vscode.dev/tunnel*"))
    OR (index=edr ((file_name="code_tunnel.json" file_path="*.vscode-cli*") OR file_name="com.visualstudio.code.tunnel.plist"))
    ```optional: relay contact on 3 or more days; a developer debugging remotely does the same```
    OR (index=proxy (url="*devtunnels.ms*" OR url="*rel.tunnels.api.visualstudio.com*"))
| eval device=coalesce(host, src_ip), day=strftime(_time, "%F")
| eval signal=case(isnotnull(process), "tunnel-command", isnotnull(file_name), "tunnel-artifact-file", true(), "relay")
| stats dc(day) AS active_days min(_time) AS first_seen max(_time) AS last_seen BY device, signal
| where signal != "relay" OR active_days >= 3
| stats values(signal) AS signals max(last_seen) AS last_seen min(first_seen) AS first_seen BY device
| sort - last_seen
| convert ctime(first_seen) ctime(last_seen)
Lower-fidelity parts (1)
  • the relay OR branch — relay contact alone is routine developer traffic. Remove: delete the ```optional comment and the OR (index=proxy ...) line under it

CrowdStrike LogScale

Written for this site, from indicators reported by ReliaQuest, Unit 42

Process command lines and relay DNS lookups per device; file artifacts are not in the Falcon events used here.

// Hunt: VS Code dev tunnel as remote access. CommandLine covers the subcommand; DnsRequest covers the relay.
(#event_simpleName=ProcessRollup2 or #event_simpleName=SyntheticProcessRollup2 or #event_simpleName=DnsRequest)
| case {
    ImageFileName=/(^|[\\\/])code(-tunnel)?(\.exe)?$/i CommandLine=/tunnel/i | signal := "tunnel-command";
    CommandLine=/code tunnel|vscode\.dev\/tunnel/i | signal := "tunnel-command";
    // optional: relay lookup alone; a developer debugging remotely does the same
    DomainName=/devtunnels\.ms$|rel\.tunnels\.api\.visualstudio\.com$/i | signal := "relay-lookup";
    * | signal := "";
  }
| signal != ""
| groupBy([aid, ComputerName], function=[collect([signal]), min(@timestamp, as=firstSeen), max(@timestamp, as=lastSeen)])
| sort(lastSeen, order=desc)
Lower-fidelity parts (1)
  • the relay-lookup branch — relay lookups alone are routine developer traffic. Remove: delete the // optional comment and the DomainName line under it

Elastic

Written for this site, from indicators reported by ReliaQuest, Unit 42, MITRE ATT&CK

Reads Elastic Defend process, file and DNS events; signals names what fired per host.

FROM logs-endpoint.events.process-*, logs-endpoint.events.file-*, logs-endpoint.events.network-*
| WHERE @timestamp > NOW() - 30 days
| EVAL cmd = TO_LOWER(COALESCE(process.command_line, ""))
| EVAL proc = TO_LOWER(COALESCE(process.name, ""))
| EVAL fpath = TO_LOWER(COALESCE(file.path, ""))
| EVAL relay = TO_LOWER(COALESCE(dns.question.name, ""))
| EVAL signal = CASE(
    proc IN ("code.exe", "code-tunnel.exe", "code", "code-tunnel") AND cmd LIKE "*tunnel*", "tunnel-command",
    cmd LIKE "*code tunnel*" OR cmd LIKE "*vscode.dev/tunnel*", "tunnel-command",
    ENDS_WITH(fpath, "/.vscode-cli/code_tunnel.json") OR ENDS_WITH(fpath, "\\.vscode-cli\\code_tunnel.json"), "tunnel-artifact-file",
    ENDS_WITH(fpath, "/library/launchagents/com.visualstudio.code.tunnel.plist"), "tunnel-artifact-file",
    "")
| EVAL signal = CASE(signal != "", signal, relay RLIKE ".*(devtunnels[.]ms|rel[.]tunnels[.]api[.]visualstudio[.]com)", "relay-lookup", "")
| WHERE signal != ""
| STATS first_seen = MIN(@timestamp), last_seen = MAX(@timestamp), signals = VALUES(signal) BY host.name
| SORT last_seen DESC
Lower-fidelity parts (1)
  • the relay-lookup EVAL line — relay lookups alone are routine developer traffic. Remove: delete the second `| EVAL signal = CASE(signal != ""` line

Cortex XQL

Written for this site, from indicators reported by ReliaQuest, Unit 42

Finds tunnel commands, tunnel artifact files and relay lookups in xdr_data per host.

// Hunt: VS Code dev tunnel as remote access.
dataset = xdr_data
| filter (event_type = ENUM.PROCESS and (action_process_image_command_line ~= "(?i)code tunnel|vscode[.]dev/tunnel"
        or (action_process_image_name ~= "(?i)^code(-tunnel)?([.]exe)?$" and action_process_image_command_line ~= "(?i)tunnel")))
    or (event_type = ENUM.FILE and (action_file_name ~= "(?i)^code_tunnel[.]json$" and action_file_path ~= "(?i)[.]vscode-cli"
        or action_file_name ~= "(?i)^com[.]visualstudio[.]code[.]tunnel[.]plist$"))
    or (event_type = ENUM.NETWORK and dns_query_name ~= "(?i)(devtunnels[.]ms|rel[.]tunnels[.]api[.]visualstudio[.]com)$")
| alter signal = if(event_type = ENUM.PROCESS, "tunnel-command", if(event_type = ENUM.FILE, "tunnel-artifact-file", "relay-lookup"))
| comp values(signal) as signals, min(_time) as first_seen, max(_time) as last_seen by agent_hostname
| sort desc last_seen

osquery

Written for this site, from indicators reported by ReliaQuest, MITRE ATT&CK

Local check for a running tunnel process and the code_tunnel.json or launch agent a tunnel leaves behind.

-- Hunt: VS Code dev tunnel as remote access; running tunnel processes and the artifacts a tunnel leaves (Windows and macOS).
SELECT 'tunnel-process' AS signal, name AS detail, cmdline AS evidence
FROM processes
WHERE (lower(name) IN ('code', 'code.exe', 'code-tunnel', 'code-tunnel.exe') AND cmdline LIKE '%tunnel%')
   OR cmdline LIKE '%code tunnel%' OR cmdline LIKE '%vscode.dev/tunnel%'
UNION ALL
SELECT 'tunnel-artifact-file', filename, path
FROM file
WHERE path LIKE '/Users/%/.vscode-cli/code_tunnel.json'
   OR path LIKE 'C:\Users\%\.vscode-cli\code_tunnel.json'
   OR path = '/Library/LaunchAgents/com.visualstudio.code.tunnel.plist'
   OR path LIKE '/Users/%/Library/LaunchAgents/com.visualstudio.code.tunnel.plist'

False positives and tuning

Dev tunnels are a supported Microsoft feature that developers use for cross-device debugging and remote containers, so a single invocation proves nothing. Abuse shows as persistence: a tunnel re-established daily or surviving reboots.

Correlate which account authorised the tunnel, often a personal GitHub identity rather than the corporate one, and whether inbound sessions arrive at times inconsistent with the employee's stated location.

Sources

Tuning required before use This hunt is a starting point. Adapt the queries to your schema, validate them against your baseline and tune them for the false positives noted above. Indicators go stale as operators rotate hosts.