Getting started
jssh gives you audited, end-to-end encrypted access to devices behind NAT: SSH, RDP, VNC, HTTP or any TCP or UDP service they declare. Each device runs a small agent that dials out to the relay (no inbound ports). You open sessions one at a time from the CLI, or turn on network mode and reach every device by name from any program.
1 · Get a device online
Create an enrollment token in the dashboard (Tokens) — it hands you a copy-paste one-liner with the token baked in. Run it on the device:
# Linux — works from a root shell or a sudo-capable user: curl -fsSL https://get.jssh.io | JSSH_ENROLL_TOKEN=<ENROLLMENT_TOKEN> sh
It auto-detects the CPU + init system, installs the agent as a service, enrolls the device, and (on Linux) enables signed auto-update — the update trust anchor ships inside the binary, so the token is all you need. Windows: from an elevated PowerShell:
iwr https://get.jssh.io/latest/install.ps1 -OutFile install.ps1 powershell -ExecutionPolicy Bypass -File .\install.ps1
Without -Token the installer prints a code you approve in the dashboard; add -Token <ENROLLMENT_TOKEN> for unattended installs.
macOS: the same one-liner as Linux — it asks for your sudo password and installs a launchd service.
Android (Termux; no root — install the packages, open a new session, then run the one-liner; sshd listens on 8022):
pkg install termux-services openssh curl -fsSL https://get.jssh.io | JSSH_ENROLL_TOKEN=<ENROLLMENT_TOKEN> sh
Docker: use the Docker tab on the token page — or run the image without a token and approve the code it prints to docker logs.
2 · Connect from your computer
curl -fsSL https://get.jssh.io/cli | sh # install the operator CLI jssh login # opens your browser to authorize this machine jssh devices # list your fleet jssh ssh <device> # or native: ssh <device>.jssh.cloud
Windows (PowerShell, no elevation needed):
iwr https://get.jssh.io/cli.ps1 -OutFile install-cli.ps1 powershell -ExecutionPolicy Bypass -File .\install-cli.ps1 jssh login
jssh login also wires up your SSH config, so you can use native tooling with no jssh prefix — ssh <device>.jssh.cloud, and the same host works with scp, rsync, and VS Code Remote-SSH:
ssh <device>.jssh.cloud # native shell (no jssh prefix) scp file.txt <device>.jssh.cloud:~/ # copy a file rsync -a ./dir <device>.jssh.cloud:/opt/ # sync a directory
jssh carries the session end-to-end encrypted; authentication is still the device's own SSH. Put your public key in the device's ~/.ssh/authorized_keys (or use a password) first, or SSH returns Permission denied (publickey).
Prefer names over commands? Section 5 brings the whole fleet into your machine's network, from the menu bar on macOS and Windows or from the Android app.
3 · Troubleshooting
- Device offline — the agent isn't connected. On the device check
systemctl status jssh-agent(orsystemctl --user status jssh-agenton Ubuntu Core) andjournalctl -u jssh-agent. The agent dials out overwss://on 443. - Permission denied — your SSH key isn't on the device yet (see step 2).
- Behind a corporate firewall — allowlist the relay host's SNI, and export
HTTPS_PROXYbefore the install command if outbound goes through a proxy. - Device won't connect at all — confirm it can reach
https://app.jssh.ioover HTTPS (443); the relay is the only outbound endpoint the agent needs. - Doctor — on the device,
jssh-agent doctorchecks DNS / 443 / TLS / proxy; from your computer,jssh doctor <device>checks your config, token, and the device's live status.
4 · Services beyond SSH
A device can expose more than sshd: declare [[service]] entries in its agent config, or enable a built-in (rdp, vnc, http, https, postgres, redis) from the device page — the device resolves the target itself; the relay never picks a destination. Connect with jssh rdp <device>, jssh vnc <device> or jssh connect <device> --service <name>; each command prints the localhost address to point your client at. With network mode (section 5) the same service answers at <device>.jssh.cloud:<port> for any program. Every service is end-to-end encrypted between the CLI and the agent — not just SSH — so the relay carries ciphertext it cannot read. The final hop on each machine is a local connection in the clear, as with an SSH port-forward.
UDP services (DNS, SNMP, syslog, WireGuard, a game server): declare the entry with type = "udp" and an explicit port — the agent needs nothing else, and no TUN or root on the device. jssh connect <device> --service <name> then listens on a local UDP port, and in network mode the port answers by name like any other. Every UDP flow to a service shares one encrypted stream through the tunnel, so datagrams arrive whole and in order (up to 64 flows per service at a time; a flow is forgotten after 30 s of silence).
[[service]] name = "dns" type = "udp" port = 53
5 · Network mode (every port, any program)
Instead of one jssh connect per service, bring the whole fleet into your machine's network: every declared, enabled port of every device you're allowed to reach answers at <device>.jssh.cloud:<port> — for curl, a browser, psql, VNC/RDP clients, dig, anything, TCP and UDP alike. Underneath it is the same end-to-end encrypted session as jssh connect, opened lazily the first time a port is used and closed after 10 minutes idle; the relay still cannot read a byte.
sudo jssh net up # macOS / Linux; needs root for the TUN device + split DNS jssh net status # no root: domain, range, open sessions, refused devices jssh net down # no root: tears everything down (routes, resolver)
Ports map to services only: a device's declared [[service]] entries plus the built-ins you enabled from its page. A port with no service is reset at once (nothing hangs), and a device whose identity key changed or that lacks an org vouch is refused — never auto-trusted — and listed by jssh net status until you resolve it.ping <device>.jssh.cloud answers only while the device is online. Nothing else changes: jssh connect, jssh ssh and native ssh keep working with no privileges at all.
No root? jssh proxy starts a local SOCKS5 proxy that resolves the same names by itself — containers, CI, locked-down laptops. Point only *.jssh.cloud at it, with a socks5h URL so the name reaches the proxy:
curl --proxy socks5h://127.0.0.1:1080 http://<device>.jssh.cloud:8080/
Menu-bar app (macOS). The same thing with a click instead of a terminal. Open it, choose Sign in… (approve in the browser), and it connects: the first time it installs its network helper, which is the one moment macOS asks for your password — after that, connecting never asks again. The menu shows the network's state, your account, your devices (each one opens to its services: Open launches the app for the service's type — Terminal for SSH, the browser for HTTP, Screen Sharing for VNC, Remote Desktop for RDP — and Copy copies the command or URL), start at login (it comes back connected if you left it that way), and settings. The CLI installer puts it in your menu bar on macOS; add -s -- --no-tray for the CLI alone:
curl -fsSL https://get.jssh.io/cli | sh
Prefer a download? jssh.dmg (universal: Apple silicon and Intel); the app carries the CLI inside and offers Install command-line tool… for your terminal. The app is signed ad-hoc (no Apple Developer ID yet), so macOS 15 asks once: System Settings → Privacy & Security → Open Anyway. The installer path skips that dialog entirely. Windows has the same tray (see below); Linux uses the CLI for now.
Android. The same network on your phone: jssh for Android (a direct APK; no store listing yet, so Android asks once to allow installs from your browser). Sign in, turn the network on (Android asks once for VPN consent), and every device answers as <device>.jssh.cloud from any app — tap a device for its services and open each one in the app that handles it (Termius or JuiceSSH for SSH, the browser for HTTP, your VNC or Remote Desktop client). Moving between wifi and mobile data reconnects by itself; a Quick Settings tile toggles it without opening the app, and the app updates itself from the same place it was installed from. One Android-specific limit: Private DNS set to a fixed provider (strict mode) bypasses the tunnel's resolver, so names don't resolve there — keep it on Automatic, or use the IP the app shows next to each device.
Windows. jssh-setup-x64.exe installs the CLI, the tray app and WireGuard's signed wintun.dll under Program Files and registers the network helper as a Windows service for every account — one administrator prompt at install, none afterwards; it adds jssh to PATH, a Start Menu entry and an uninstaller, and takes /VERYSILENT for IT. It is not code-signed yet, so SmartScreen asks once (More info → Run anyway). The tray then works like the macOS one: sign in and connect; the service owns the adapter, the route and the DNS rule and hands packets to the app running as you. No administrator rights? The PowerShell one-liner installs the same three files per user (-NoTray for the CLI alone); the tray then asks for the helper once. From a terminal, jssh net up needs no elevation once the service exists. Names resolve through a Name Resolution Policy rule the service installs and removes. IPv6: add --ipv6 to also answer AAAA with a unique-local address per device (off by default; every client falls back to A).
Known limits, so you don't chase ghosts:
- Browsers with their own DNS — Firefox in strict DNS-over-HTTPS mode (and Chrome with a forced DoH provider) bypass the system resolver, so the name never reaches the split DNS. Use the default/"increased protection" mode, or exclude
*.jssh.cloudin the browser's DoH exceptions. - Web UIs that validate
Host— some device UIs only accept their own IP/hostname and answer 400/redirect to it. Reach those viajssh connect <device> --service http(localhost) instead. - With the network down —
*.jssh.cloudpublicly resolves to your own machine (127.0.0.1); some home/office routers filter that (rebind protection), so the name resolves to nothing there. Either way nothing leaves your computer. - Corporate full-tunnel VPNs — clients that claim every route (AnyConnect-style) can black-hole the virtual range.
sudo jssh net upwarns when another interface already owns it; disconnect that VPN or usejssh proxy. - Docker Desktop and WSL2 run in their own VM with their own network: containers and WSL shells don't see the TUN. Run
jssh proxyinside them, or reach the host'sjssh connectport.
6 · Manual install (unsupported init)
The installer supports systemd, OpenRC, procd, SysV, launchd, Termux and Batocera — on anything else it refuses BEFORE enrolling, so your token isn't spent. Manually: download the binary for your arch from get.jssh.io/latest, run jssh-agent enroll (browser approval), then wire jssh-agent run into your init with a restart-on-failure policy. Service templates live in the client repo's deploy/ directory.