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
- 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.
| 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?
| 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.
- Confirm nothing is listening.
ss -ltnpornetstat -anshows no listening socket owned byjssh-agent. Scan the site's public address from outside and you will find nothing new open. - Confirm the shape of the connection.
ss -tnpshows one established outbound connection fromjssh-agentto port 443. Your firewall log shows the same thing from the other side. - Run the agent's own check.
jssh-agent doctoron 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 isjssh doctor, so "command not found" usually means you are on the other machine. - Read the unit file. The
ExecStartline names the exact local target the agent was installed with, which is the ground truth for what it can reach. - 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.iofrom 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
pfdropping UDP on the operator's Mac andiptablesdropping 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.