Access is per-device. A paired device mints a short-lived bearer token over its already-authenticated Noise session ({t:'auth.token'}) and presents it as Authorization: Bearer <token> on every /api/* HTTP request and on the leftover /api/ws upgrade. The live terminal stream is /api/noise/session, where the handshake is the authentication. There is no shared password, no tether set-password, and no /api/setup TOFU flow.
authMiddleware verifies the token (HMAC signature, expiry, and that the device is still in the registry). Revoking a device is enough: the next request with its token is 401. Public exceptions are /api/status (discovery) and the /api/noise/* handshake sockets — those authenticate via Noise itself.
The terminal stream is end-to-end encrypted over Noise (/api/noise/session). REST confidentiality is TLS.
The server opens two listeners:
| Listener | Default port | Env | Encryption |
|---|---|---|---|
| Plaintext HTTP | 8085 |
TETHER_PORT |
none |
| HTTPS / WSS | 8443 |
TETHER_TLS_PORT |
TLS, self-signed cert |
On its first boot the server generates a self-signed ECDSA P-256 certificate into ~/.tether/config/tls/ (cert.pem, key.pem — the key is 0600 inside a 0700 directory) and serves TLS from it. It is valid for ten years, and it is never rotated automatically: clients pin its fingerprint, so silently minting a new one would lock every paired client out. If the directory is half-populated, the server refuses to start TLS rather than regenerate.
Both listeners serve the same API. That is deliberate — see Migrating to TLS.
The certificate is self-signed, so no CA will vouch for it. Trust comes from pinning on first contact, the same kind of decision Noise pairing already makes for the server's static key:
-
GET /api/status(unauthenticated) returns{ "secure": true, "tls": { "enabled": true, "plaintext": true, "port": 8443, "fingerprint": "sha256:86c3fb2a…" } } -
securereports whether that response arrived over the TLS listener. It is derived from the socket, never from a header —X-Forwarded-Protois ignored on purpose, because anything a caller can claim a MITM can claim too. -
A client may only pin a fingerprint it read with
secure: true, and must compare it against the certificate that actually terminated the connection. Over plaintext the fingerprint is discovery data only — useful for finding the HTTPS port, worthless as proof, because whoever can read the plaintext can rewrite it. -
Once pinned, a fingerprint mismatch is a hard failure. It is not a re-pair prompt: the certificate does not change on its own, so a change means either an attacker or an operator who deliberately deleted the TLS directory.
Pinning on first contact is only as good as that first contact. Pair devices (tether pair) on a network you trust — the LAN, or a tunnel.
TETHER_TLS picks the listener topology. It is not in /api/config, and cannot be changed by a client: a phone that closed the plaintext port would lock out every other client on the network, and the only recovery would be shell access to the host. /api/config and /api/status report the transport state; only the host's environment sets it.
TETHER_TLS |
Result |
|---|---|
unset / both (default) |
plaintext and TLS |
only |
TLS only — the plaintext port is closed |
off |
plaintext only — no certificate is generated or served |
The default keeps the plaintext port open, so a tether update changes nothing for a client that has not learned TLS yet. When every client you use has paired over HTTPS, cut over with TETHER_TLS=only tether restart and the plaintext port closes.
Two things to know about only:
tether statusandtether presentfollow the same setting, so they keep working over loopback TLS.- Any client still on
http://stops working the moment you flip it. That is the point of it being a deliberate, host-side switch.
TETHER_TLS_EXTRA_NAMES (comma-separated) adds extra SANs — a Tailscale name, say — but only on the boot that generates the certificate. Adding names later means deleting ~/.tether/config/tls/ and re-pairing every client.
The sensitive files the server writes are locked to the owner: the database in ~/.tether/config/ (session log + device registry), the TLS private key, the Noise identity and auth-token HMAC secret (~/.tether/config/noise/), the holder sockets, and the local control token.
The key is 0600 inside a 0700 directory, and the other paths are created owner-only the same way.
The guarantee: another user on the machine cannot read your device registry, Noise keys, or TLS key.
TLS closes the "the wire is readable" hole for REST. It does not make the port safe to expose:
- The certificate is self-signed, so an unpinned client has no way to detect a MITM on first contact.
- CORS is still
*, and the API exposes file read, file upload, and git write operations. - A remote shell is a high-trust surface either way.
So: keep Tether on the LAN, or behind Tailscale / WireGuard / an SSH port-forward. TLS is defence in depth on top of that, not a replacement for it, and it is what makes an accidental exposure survivable instead of catastrophic. The terminal itself is E2E over Noise even on plaintext HTTP.
::: warning A remote shell is a high-trust surface. Anyone with a paired device (or its token) and network reach gets a shell. Do not expose the port directly to the internet. :::