Security

What jssh can see, and what it cannot

Written for the person who has to say "yes" before an agent lands on their fleet. Four facts first, then the drawing, then every question a review has asked us so far.

The short version

Four facts, then the working

Session content
never stored SSH is end to end to your own sshd
Open ports
none one outbound TLS connection on 443
Device to device
no route nothing was ever wired between them
SOC 2
no report and we will not imply one

SSH stays end-to-end encrypted between the operator's ssh client and the device's own sshd. The relay and the agent move opaque ciphertext, so neither can read session content or credentials. What we audit is metadata: who connected to which device, when, and for how long.

The geometry

There is no path between two devices

Fig. 1 One isolated path per device

Fig. 1. One isolated path per device. Each device gets its own isolated relay endpoint, and nothing was ever wired between two of them, so there is no route to misconfigure and none to disable. SSH stays end-to-end encrypted between the technician's client and the machine's own sshd, which authenticates him with the keys it already had, so on those sessions the relay moves opaque ciphertext; whether an RDP or VNC session is encrypted is a property of the service running on the machine. The drawing shows one operator on the left, four separate relay cells in the middle with a gap and a cross mark between each pair, and on the right four devices at customer sites: store-kiosk-12 reached over RDP, gateway-customer-42 reached over SSH, lobby-pi-3 reached over VNC, factory-gw-01 reached over SSH. Each device holds one outbound TCP 443 connection out through its own NAT, CGNAT or firewall, and nothing is listening on ports 22, 3389 or 443 at the site. A direct path is used when UDP is open, measured at 7.4 milliseconds; the relayed path over TCP 443 was measured at 33 milliseconds. You The relay Your customers' sites $ Operator No route SSH bytes the relay cannot read Outbound TCP 443 only NAT · CGNAT · firewall store-kiosk-12 RDP 22 · 3389 · 443 gateway-customer-42 SSH 22 · 3389 · 443 lobby-pi-3 VNC 22 · 3389 · 443 factory-gw-01 SSH 22 · 3389 · 443 Nothing listens Direct path when UDP is open · 7.4 ms p50 Via relay on TCP 443 · 33 ms Fig. 1. One isolated path per device. Each device gets its own isolated relay endpoint, and nothing was ever wired between two of them, so there is no route to misconfigure and none to disable. SSH stays end-to-end encrypted between the technician's client and the machine's own sshd, which authenticates him with the keys it already had, so on those sessions the relay moves opaque ciphertext; whether an RDP or VNC session is encrypted is a property of the service running on the machine. The drawing shows one operator on the left, four separate relay cells in the middle with a gap and a cross mark between each pair, and on the right four devices at customer sites: store-kiosk-12 reached over RDP, gateway-customer-42 reached over SSH, lobby-pi-3 reached over VNC, factory-gw-01 reached over SSH. Each device holds one outbound TCP 443 connection out through its own NAT, CGNAT or firewall, and nothing is listening on ports 22, 3389 or 443 at the site. A direct path is used when UDP is open, measured at 7.4 milliseconds; the relayed path over TCP 443 was measured at 33 milliseconds. $ Operator The relay Customers' sites No route NAT · CGNAT · firewall store-kiosk-12 RDP 22 · 3389 · 443 gateway-customer-42 SSH 22 · 3389 · 443 lobby-pi-3 VNC 22 · 3389 · 443 factory-gw-01 SSH 22 · 3389 · 443 Nothing listens SSH bytes the relay cannot read Outbound TCP 443 only
  • An authorized session path, one per device.
  • The session's bytes, carried through one cell. The relay cannot read SSH. Whether an RDP or VNC session is encrypted is a property of the service running on the machine.
  • No route. The cells were never joined.
  • The customer's network boundary.
  • 7.4 ms on the direct path when UDP is open, against 33 ms via the relay on TCP 443.

The review

Every question a security review has asked us

Collapsed so you can find yours. Each one has a stable link, so you can send the answer rather than the page.

01What does the agent open on our network?

Nothing. The agent makes a single outbound TLS connection on 443 and listens on no port, so there is nothing to forward and no public IP to buy. A scan of that network from the internet finds no jssh.

The one network shape this does not cover is a site whose only route out is an explicit HTTP proxy: the relay connection does not speak HTTP CONNECT today, so it needs a direct allowlist entry for app.jssh.io on TCP 443. Everything a Question 02 is the destination table, and the change-ticket text is in it.

02Exactly which destinations does it connect to?

Three hostnames, outbound only, plus an optional direct path. No inbound rules and no changes to NAT or to your router. The device works behind CGNAT and behind a firewall that permits nothing inbound at all.

Table 2 As shipped Every remote destination the agent connects to. Read from the agent's own source and the relay's, client v0.2.32, on 28 July 2026. jssh.io/security/#what-it-needs
Purpose Destination Protocol and port Required
Control connection, held open app.jssh.io TCP 443, TLS Yes
Session data, preferred path turn.cloudflare.com, then UDP directly to the technician's public address, which is not known in advance UDP 3478, then UDP No
Session data, used when UDP is blocked turn.cloudflare.com TCP 443, TLS Yes if you block UDP
Signed agent updates get.jssh.io TCP 443, TLS Yes while auto-update is on

If your egress policy is outbound TCP 443 and nothing else, every part of this works. The agent probes UDP once, finds it blocked, and runs the session over TLS on 443 for the rest of that session.

Text for a change ticket, if you need one:

Allow outbound from <device> to:  app.jssh.io          TCP 443 (TLS)   required, held open  turn.cloudflare.com  TCP 443 (TLS)   required if UDP is blocked  turn.cloudflare.com  UDP 3478        optional, lowers latency  get.jssh.io          TCP 443 (TLS)   required while auto-update is onNo inbound rules. No NAT or router changes.

One limitation we would rather you hear from us

The agent's relay connection does not speak HTTP CONNECT today, so it cannot traverse a proxy-only egress. jssh-agent doctor prints a line reading proxy: HTTPS_PROXY=… (honored) when that variable is set, and that line describes the diagnostic's own HTTP client rather than the agent's connection. If your site allows outbound only through an explicit proxy, say so before the install, because the check will look green and the device will never appear.

03What can it reach on the device itself?

Exactly what is in a service registry on the machine. Each entry is a name, a type and a host:port that defaults to 127.0.0.1. A fresh install has one entry, ssh, pointing at 127.0.0.1:22. On Android under Termux the default is 127.0.0.1:8022, because Android forbids unprivileged binds below 1024. The usual second and third entries are 127.0.0.1:3389 for Windows RDP and 127.0.0.1:5900 for VNC.

That registry is the allowlist and the agent on your hardware enforces it. A request for a service that is not registered, or is registered and disabled, is refused on the device before any byte moves. Where that check runs is the whole point: on the direct path our relay never learns which service was asked for, so the only place the check can be made is on your hardware, which is where it is made.

Adding a service means editing a file on that machine, which needs the access to it you already control. Nobody can widen the list from a dashboard. Access rules on our side narrow it further: a rule can grant one named technician one named service on one named machine, and the agent is again what enforces it.

04Can one compromised device reach another?

No, and the reason is that nothing was ever wired between them. Each device gets its own isolated relay endpoint, so there is no network between devices and no route to disable. A device that is taken over can reach its own endpoint and gets nowhere from there, in any direction, including into another customer's fleet. Fig. 1 above draws it.

Tenant to tenant, every connection is authorized against the org's access rules before it reaches a device. Cross-tenant isolation is covered by the test suite (tests/db.test.ts, tests/e2e.test.ts and tests/failure-events.test.ts each assert that another org's device answers as if it did not exist), and those tests run on the machine that ships the deploy. There is no hosted CI in front of the relay repository today, so nobody should read this as an automated release gate.

05What do you actually store?
Table 1 As shipped What the relay holds about a session and what it never receives. Read from the relay's own source and the agent's, client v0.2.32, on 28 July 2026. The first row is also a written commitment: that session content is never recorded or stored on any path is stated in the Privacy Notice at app.jssh.io/privacy, last updated 27 July 2026, which is the document to quote. jssh.io/security/#what-we-store
Data What we hold
Session content (keystrokes, files) Never. End-to-end encrypted, and the relay only sees ciphertext
SSH credentials and private keys Never. They stay on your side
Session traffic volume (bytes transferred) Never. The audit log has no bytes field
Session metadata (who, device, when, duration, source IP) Yes. This is the audit log
Device identity (public key, name, tags, last seen) Yes

What that log looks like, and what one export contains, is in the audit questions, with the file itself.

06How are operators and devices authenticated?

Authentication is mutual. Devices hold an Ed25519 keypair and we store only the public key. Operators authenticate through your identity provider over OIDC when single sign-on is configured, or by email link when it is not.

Single sign-on is on every plan, including the trial. An organization points jssh at its own OpenID Connect provider, which is how Okta, Entra ID, Google Workspace and Auth0 are reached, and the email domain it claims for that provider has to be proven by a TXT record published under _jssh-verify in that domain's own DNS. Until that record resolves, the provider is not offered to anyone signing in at an address in that domain.

07What is the agent, and can it run code you push?

It is a static Rust binary with no runtime dependencies, about 4 MB on ARM and about 5 MB on x86_64. The Linux builds link musl statically, so nothing on the device has to match it. It proxies bytes to the local ports it is configured to expose (localhost:22 for SSH).

It does not execute remote commands pushed from the cloud, and your device's own sshd stays a second, independent authentication layer. Updates are signed release artifacts the agent pulls and verifies, staged A/B, with automatic rollback if the new binary fails to come up.

08How do updates reach it?

Auto-update is on by default and can be turned off at install time. When it is on, the agent fetches signed release metadata from get.jssh.io. The signatures chain to a root public key compiled into the binary already on the device, so a compromised download server cannot make it install anything, and the verifier rejects a withheld or replayed older release. Downloads are verified by SHA-256 at install time as well.

A new version installs into a second slot. A small launcher counts boot attempts, and if the new build fails to come up it flips back to the previous slot, leaving the device online on the old version. That rollback is one of twelve installation cases we re-run before each release, drilled with a genuinely signed build rigged to fail at boot. Eleven of the twelve can fail the run; the ARMv6 case only reports, which is why ARMv6 is listed as published and not validated.

If your policy is that nothing on the machine updates itself, install with auto-update disabled and update it by hand.

09How do we verify all of this ourselves?

Do not take any of this on trust. Everything above is observable from the machine, and none of it requires an account with us.

  1. Confirm nothing is listening. ss -ltnp or netstat -an shows no listening socket owned by jssh-agent. Scan the site's public address from outside and you will find nothing new open.
  2. Confirm the shape of the connection. ss -tnp shows one established outbound connection from jssh-agent to port 443. Your firewall log shows the same thing from the other side.
  3. Run the agent's own check. jssh-agent doctor on the device resolves the relay hostname, opens a TCP connection on 443 and performs an HTTPS GET over TLS, printing each step as a pass or a failure. It proves the device can reach the relay and says nothing about what the agent then does, which is why it is the first of these steps and not the last. A technician's laptop runs a different binary whose command is jssh doctor, so "command not found" usually means you are on the other machine.
  4. Read the unit file. The ExecStart line names the exact local target the agent was installed with, which is the ground truth for what it can reach.
  5. Keep watching it. The agent is one process holding one outbound socket, so anything else it started doing would show up in the tooling you already run.
10How do we cut access, and what happens to a live session?

There are two independent ways, and one of them is yours alone.

  • From your side, immediately, without asking anyone. Stop the service, or block outbound 443 to app.jssh.io from that machine. The agent cannot function without the connection it dials out, and it has no other way in.
  • From the dashboard. Revoking the device closes every connection it has open, including a session in progress at that moment, and its reconnect is then refused. Revoking a person closes the sessions that person has open. A single live session can also be ended on its own without revoking anything.

In both cases the machine keeps its own sshd, its own host keys and its own users. Nothing about it was converted in order to use jssh, so nothing has to be converted back.

11How much latency does the relay add?

Every number below comes from jssh ping, which times the round trip of one keystroke-sized packet from the operator to the device and back, reported as the median of a run. The pair was a macOS laptop and a Linux host, both in Argentina, on client v0.2.32.

  • 7.4 ms p50 on a direct peer-to-peer path negotiated by UDP hole punching.
  • 24.6 ms p50 through Cloudflare's anycast TURN relay, min 18 ms, p95 30 ms.
  • About 33 ms over TURN on TLS port 443 with UDP genuinely blocked, enforced by pf dropping UDP on the operator's Mac and iptables dropping it on the device.
  • About 0.6 ms with both machines on the same LAN.

These are single-pair measurements on specific network paths, they were not re-run on every release, and they are not a service level agreement.

12Where does our data sit?

We do not offer a data-residency guarantee today. jssh runs on Cloudflare's global network, and nothing in the platform pins or partitions storage by region yet. Your organization can record a region in the dashboard, but that field is a label for your own reference and it does not route anything.

If residency is a hard requirement for your procurement, write to us before you deploy so we can tell you exactly where your data sits. Our data processing addendum is published at app.jssh.io/dpa.

13Do you have a SOC 2 report?

No, and we will not imply one. There is no SOC 2 report today and no audit in progress that we would be entitled to name. If your procurement requires it, telling us moves it up the list.

What exists today and can be read by a reviewer is this page, the Data Processing Agreement, the Privacy Notice, and a compatibility matrix that still has its gaps written into it.

14How do we report a vulnerability?

Mail security@jssh.io. Good-faith research gets a reply from a person who works on jssh, and no legal threats. We will credit you if you want it.

Send this page, or ask the question it did not answer

It needs no login and it carries no pricing, so it forwards as it is. A question that is not above probably belongs on it.