devtunnel-ssh

module
v0.0.0-...-227bce8 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 9, 2026 License: MIT

README

dtssh — seamless SSH over Microsoft Dev Tunnels

dtssh turns a Microsoft Dev Tunnel into a transparent SSH transport. It runs a dedicated, hardened OpenSSH server on the host (bound to loopback, reachable only through the tunnel), and wires up the client so that a plain ssh <alias> — plus scp, git, rsync, and VS Code Remote-SSH — just works across NAT/firewalls with no manual SSH authentication, no password prompts, and no host-key prompts.

  ┌────────────┐   ssh <alias>    ┌──────────────┐   dev tunnel relay   ┌───────────────┐
  │  client    │ ───────────────▶ │ dtssh proxy  │ ═══════════════════▶ │  dtssh host   │
  │  (ssh/scp) │  ProxyCommand    │ (stdio↔TCP)  │   (devtunnel CLI)    │ dedicated sshd│
  └────────────┘                  └──────────────┘                      └───────────────┘

Why

Dev Tunnels give you an authenticated, internet-reachable relay to a local TCP port without opening inbound firewall ports. SSH gives you a battle-tested, end-to-end-encrypted session with a rich ecosystem (scp/sftp/git/port-forwarding/ VS Code). dtssh marries the two so the tunnel hop is invisible to every tool that speaks SSH.

How it works

  1. Host (dtssh host)

    • Generates an ephemeral SSH keypair and a dedicated host key.
    • Writes a hardened sshd_config and launches an unprivileged sshd instance bound to 127.0.0.1:<port> (does not touch the system SSH service).
    • Hosts that port over a dev tunnel via the devtunnel CLI.
    • Publishes pairing metadata (alias, user/port, host public key, and the ephemeral key seed) in the tunnel's description under a dtssh label, so a logged-in client can find and connect to it with no bundle (see Zero-import discovery).
    • Also prints a compact connect bundle for clients that don't share your devtunnel login.
  2. Client — two ways to pair, both fully automatic afterwards:

    • dtssh discover — finds every dtssh host on your devtunnel account and wires up ssh <alias> for each. No bundle to copy.
    • dtssh import <bundle> — paste the printed bundle (for clients with no shared devtunnel login).

    Either way, dtssh installs the private key, pins the host key in a managed known_hosts, and adds a Host <alias> block to ~/.ssh/config whose ProxyCommand is dtssh proxy.

  3. dtssh proxy (the ProxyCommand, wired up automatically)

    • Ensures a single shared devtunnel connect forwarder is running and pipes ssh's stdin/stdout to the tunnel's loopback port. One forwarder serves all concurrent sessions (important for VS Code Remote-SSH, which opens several).

The result: ssh <alias> performs standard SSH public-key auth over the tunnel. "No extra SSH auth" means the tooling provisions and delivers the key for you — not that SSH authentication is disabled.

Zero-import discovery

When both machines are logged into the same devtunnel account (devtunnel user login), you never touch a bundle:

# Host:
dtssh host                 # non-anonymous + discoverable by default

# Client:
dtssh discover             # finds the host, wires up ssh <alias>
ssh dt-<host>

dtssh host tags its tunnel with the dtssh label and stores compact pairing metadata — including the ephemeral key seed and the host public key — in the tunnel description. dtssh discover runs devtunnel list --labels dtssh, reconstructs the SSH key from the seed, pins the host key, and installs the ssh_config alias. Re-run dtssh discover any time to pick up new hosts (add --prune to drop aliases whose tunnels are gone).

Because a tunnel description is only account-private on non-anonymous tunnels, discovery metadata (which contains a secret seed) is published only for non-anonymous tunnels. dtssh host --anonymous opts back into the classic bundle-only flow and publishes no seed.

Install

Requires OpenSSH (ssh, ssh-keygen; plus sshd on the host). The devtunnel CLI is auto-downloaded on first use (dtssh login/host/discover), so you don't need to install it separately — though a copy already on PATH (or DTSSH_DEVTUNNEL) is used if present.

With a Go toolchain (1.21+), install the latest dtssh straight from the repository — no manual clone required:

go install github.com/bmiddha/devtunnel-ssh/cmd/dtssh@latest

This drops the dtssh binary in $(go env GOPATH)/bin (usually ~/go/bin). Make sure that directory is on your PATH:

export PATH="$(go env GOPATH)/bin:$PATH"   # add to your shell profile

To pin a specific version, replace @latest with a tag, e.g. @v0.1.0.

Build from a clone
git clone https://github.com/bmiddha/devtunnel-ssh
cd dtssh
go build -o dtssh ./cmd/dtssh
install -m755 dtssh ~/.local/bin/dtssh    # or use scripts/install.sh

On Windows, run scripts/install.ps1 from a cloned checkout.

Check your environment any time:

dtssh doctor

Usage

On the host (the machine you want to reach)
dtssh login              # once — auto-downloads the devtunnel CLI, then logs in
dtssh host               # defaults: dedicated sshd on 127.0.0.1:2222, anonymous tunnel

dtssh login fetches the official Microsoft Dev Tunnels CLI for your platform (if it isn't already on PATH) into dtssh's cache dir, then runs devtunnel user login. Any flags are passed straight through, e.g. dtssh login -d (device-code auth for headless hosts) or dtssh login -g (GitHub). The other commands (host, service, discover) auto-download the CLI the same way, so you never have to install it manually.

dtssh host prints something like:

 On the CLIENT machine, run:

     dtssh import 'dtssh1:eyJ2Ijox...'

 then simply:

     ssh dt-myhost

Useful flags: --port, --user, --alias, --tunnel <id> (reuse a persistent tunnel), --expiration 8h, --anonymous (classic bundle-only flow; disables discovery), --discoverable=false (host privately without publishing pairing metadata), --system-sshd (use the existing system sshd instead of a dedicated instance).

Run the host as an always-on service

To keep a machine reachable without leaving a terminal open, register the host as a per-user background service that starts at login and auto-restarts on failure (auto-recovery). No admin rights are needed:

dtssh login                         # once (auto-downloads the devtunnel CLI)
dtssh service install --port 2222   # same flags as `dtssh host`
dtssh service status                # or: logs, restart, stop, start
dtssh service uninstall

The service hosts in --persist mode: it reuses a single tunnel and a stable identity across restarts, so recovery never accumulates stale tunnels or breaks discovery. Under the hood it uses the platform's native user supervisor:

OS Mechanism Auto-recovery
Linux systemd user unit (~/.config/systemd/user/dtssh-host.service) Restart=always
macOS launchd LaunchAgent (~/Library/LaunchAgents/com.bmiddha.dtssh-host.plist) KeepAlive
Windows Task Scheduler task (\dtssh-host, logon + boot triggers) RestartOnFailure

On Linux, dtssh service install also tries loginctl enable-linger so the host keeps running after you log out / across reboots.

WSL — running the host inside a WSL distro is fully supported and handled specially:

  • No name collision. Inside WSL, the hostname is the Windows computer name, so a WSL host and its Windows host would otherwise derive the same alias. dtssh detects WSL and qualifies the alias/tunnel with the distro, e.g. dt-mypcdt-mypc-wsl-ubuntu.
  • Cold-start auto-boot. A WSL distro doesn't run until something launches it, so a systemd user service inside WSL wouldn't come back after a Windows reboot. dtssh service install (when it detects WSL) also drops a hidden launcher in the Windows per-user Startup folder that boots the distro at logon — no admin, no Task Scheduler policy needed. With systemd=true in /etc/wsl.conf and lingering enabled, the boot brings up the host service automatically. Opt out with --no-wsl-boot; dtssh service uninstall removes the launcher.
On the client (the machine you connect from)
# Same devtunnel account as the host — zero import:
dtssh discover               # finds hosts, wires up ssh <alias> for each
ssh dt-myhost                # seamless: no password, no key setup, no host prompt

# Or, no shared login — paste the bundle once:
dtssh import 'dtssh1:...'

scp file dt-myhost:~/
git clone ssh://dt-myhost/path/to/repo.git
code --remote ssh-remote+dt-myhost /home/you/project   # VS Code Remote-SSH

Undo with dtssh remove dt-myhost. Housekeeping flags for dtssh discover: --prune (drop local managed aliases whose tunnels are gone) and --clean (delete superseded stale tunnels — offline duplicates of a live host — from your devtunnel account; a lagging or sole host is never touched).

Command reference

Command Description
dtssh login [flags] Auto-download the devtunnel CLI (if missing) and run devtunnel user login; flags pass through (-d device code, -g GitHub, -e Entra).
dtssh host Expose this machine over a dev tunnel; publish discovery metadata + print a bundle.
dtssh service install|uninstall|start|stop|restart|status|logs Run the host as an auto-restarting, login-started user service.
dtssh discover Auto-discover dtssh hosts on your account and wire up ssh <alias> (no import). --prune drops local aliases whose tunnels are gone; --clean deletes superseded stale tunnels from your account; -q for names only.
dtssh import <bundle> Install a bundle so ssh <alias> works (for clients with no shared login).
dtssh list List dtssh-managed host aliases (add -q for names only).
dtssh remove <alias> Undo an import/discover (config, key, known_hosts pin).
dtssh connect <alias> [args] Thin ssh wrapper.
dtssh proxy <tunnel> ProxyCommand (used automatically).
dtssh forward status|stop <tunnel> Manage the shared client forwarder.
dtssh config <tunnel> Print an ssh_config block.
dtssh doctor Check devtunnel/ssh/sshd availability.

Platform notes

  • Linux / macOS — dedicated-sshd mode works out of the box (no root needed; the instance can only log in the user who launched it). sftp-server is auto-detected for scp/sftp support.
  • Windows — the client side (import, proxy, ssh) works with the built-in OpenSSH client. For hosting, running a second unprivileged sshd is awkward, so use --system-sshd: enable the OpenSSH Server optional feature, then dtssh host --system-sshd appends the ephemeral key to your %USERPROFILE%\.ssh\authorized_keys.

See docs/architecture.md for the full design, security model, and the concurrency/limitations discussion.

Files & directories

dtssh follows the XDG Base Directory spec on Linux/macOS (no home-dir dotfolder) and uses %LOCALAPPDATA% on Windows. Files are split by category:

Category Linux/macOS (XDG) Windows Contents
data $XDG_DATA_HOME/dtssh (~/.local/share/dtssh) %LOCALAPPDATA%\dtssh host & client keys, authorized_keys, sshd config, service identity seeds
state $XDG_STATE_HOME/dtssh (~/.local/state/dtssh) %LOCALAPPDATA%\dtssh logs, per-tunnel forwarder metadata (pid/port/lock)
cache $XDG_CACHE_HOME/dtssh (~/.cache/dtssh) %LOCALAPPDATA%\dtssh auto-downloaded devtunnel CLI (re-fetchable)

Set DTSSH_HOME to override everything into a single flat root (handy for tests or portable setups). An existing legacy ~/.dtssh directory is migrated to the XDG locations automatically on first run. The client ssh_config block and known_hosts pins live under ~/.ssh as usual. dtssh doctor prints the resolved directories.

Troubleshooting

Set DTSSH_DEBUG=1 to emit timestamped diagnostics on stderr for the host, proxy, and forwarder. Debug output never touches stdout, so it is safe to enable even while dtssh proxy is serving an SSH session.

# See exactly which tunnel id the host bakes into the bundle:
DTSSH_DEBUG=1 dtssh host

# Trace the client hop (forwarder start, parsed local port, bridge target):
DTSSH_DEBUG=1 ssh <alias>          # ssh -v also shows the ProxyCommand's stderr

If ssh <alias> hangs or errors with "devtunnel connect failed … Tunnel not found", the bundle was created by an outdated dtssh host. Update dtssh on the host, re-run dtssh host, and re-import the fresh bundle. A valid connect id looks like tidy-fog-btv9lwz.usw2 (id + cluster), not the short subdomain in the browser URL (9d6v4rxw).

Security model

  • The dev tunnel is the only ingress: sshd listens on loopback.
  • Two independent layers of auth: the tunnel (login or connect token) and SSH public-key auth. Anonymous tunnels still require the SSH key.
  • Host identity is pinned on import (StrictHostKeyChecking yes against a dtssh-managed known_hosts), so there is no trust-on-first-use prompt and MITM of the relay cannot silently substitute a host key.
  • Host-key rotation self-heals. A host key normally never changes (it is reused from disk across restarts), but it can change if the host's data dir is wiped/reinstalled/moved, you toggle --system-sshd, or you re-host the same alias on a different machine. Rather than failing with the scary REMOTE HOST IDENTIFICATION HAS CHANGED error, dtssh proxy re-fetches the authoritative host key from your devtunnel account metadata (TLS-served to the owning account, unreachable by a relay MITM) on cold reconnect and refreshes the pin — printing host key ... changed; refreshed the pin from your devtunnel account. This only applies to discoverable tunnels; bundle-imported/anonymous hosts keep a strict pin and must be re-imported if their key legitimately rotates.
  • Keys are ephemeral, per-host, stored 0600 under the dtssh data dir (see Files & directories). The bundle contains a private key — treat it like a password and share it over a trusted channel.

Development

go build ./...
go test ./...
go vet ./...

Licensed under the MIT License. This project uses the Microsoft devtunnel CLI and OpenSSH; it is not affiliated with or endorsed by Microsoft.

Directories

Path Synopsis
cmd
dtssh command
Command dtssh provides seamless SSH sessions over Microsoft Dev Tunnels.
Command dtssh provides seamless SSH sessions over Microsoft Dev Tunnels.
internal
bundle
Package bundle defines the portable "connect bundle" that carries everything a client needs to SSH into a host over its dev tunnel: the tunnel id, the target port/user, the ephemeral private key, and the host public key for pinning.
Package bundle defines the portable "connect bundle" that carries everything a client needs to SSH into a host over its dev tunnel: the tunnel id, the target port/user, the ephemeral private key, and the host public key for pinning.
commands
Package commands implements the dtssh subcommands.
Package commands implements the dtssh subcommands.
debug
Package debug provides opt-in diagnostic logging, enabled by setting the DTSSH_DEBUG environment variable to a non-empty, non-"0"/"false" value.
Package debug provides opt-in diagnostic logging, enabled by setting the DTSSH_DEBUG environment variable to a non-empty, non-"0"/"false" value.
devtunnel
Package devtunnel is a thin wrapper around the Microsoft `devtunnel` CLI, exposing just the two flows dtssh needs: hosting a TCP port and establishing a client-side loopback forward.
Package devtunnel is a thin wrapper around the Microsoft `devtunnel` CLI, exposing just the two flows dtssh needs: hosting a TCP port and establishing a client-side loopback forward.
discovery
Package discovery implements automatic, zero-import pairing: hosts tag their dev tunnel with a well-known label and stash a compact metadata blob (alias, port, user, host public key, and the ephemeral SSH seed) in the tunnel description.
Package discovery implements automatic, zero-import pairing: hosts tag their dev tunnel with a well-known label and stash a compact metadata blob (alias, port, user, host public key, and the ephemeral SSH seed) in the tunnel description.
forward
Package forward manages a lazy, persistent client-side forwarder built on `devtunnel connect`.
Package forward manages a lazy, persistent client-side forwarder built on `devtunnel connect`.
keys
Package keys manages the ephemeral SSH identities used by dtssh.
Package keys manages the ephemeral SSH identities used by dtssh.
paths
Package paths resolves the cross-platform locations where dtssh keeps its files.
Package paths resolves the cross-platform locations where dtssh keeps its files.
proxy
Package proxy implements the stdio<->TCP bridge used as an OpenSSH ProxyCommand.
Package proxy implements the stdio<->TCP bridge used as an OpenSSH ProxyCommand.
service
Package service registers dtssh as a per-user, auto-restarting background service so a host machine keeps itself reachable over a dev tunnel without a human running `dtssh host` in a terminal.
Package service registers dtssh as a per-user, auto-restarting background service so a host machine keeps itself reachable over a dev tunnel without a human running `dtssh host` in a terminal.
sshconfig
Package sshconfig installs and removes the per-alias OpenSSH client configuration and known_hosts pinning that make `ssh <alias>` transparently hop through a dev tunnel with no prompts.
Package sshconfig installs and removes the per-alias OpenSSH client configuration and known_hosts pinning that make `ssh <alias>` transparently hop through a dev tunnel with no prompts.
sshd
Package sshd manages a dedicated, unprivileged OpenSSH server instance that is bound to the loopback interface and only reachable through the dev tunnel.
Package sshd manages a dedicated, unprivileged OpenSSH server instance that is bound to the loopback interface and only reachable through the dev tunnel.
wsl
Package wsl detects the Windows Subsystem for Linux and bridges to the Windows host when dtssh runs inside a WSL distro.
Package wsl detects the Windows Subsystem for Linux and bridges to the Windows host when dtssh runs inside a WSL distro.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL