Security & privacy
Describes agent 2.9.90 · app 2.9.50 · last verified against the source August 2026
Your phone talks directly to an open-source service on your own machine, over your own LAN. Auth is a per-box bearer token you get by scanning the box's screen. Traffic is encrypted with a certificate pinned at pairing. There is no cloud, no account, and no analytics. Everything on this page is checkable against the source, and the last section says plainly what this design does not protect against.
Architecture
Two pieces: the phone app and couchsided.py, a single-file Python service (standard library only, no third-party imports) running on your box. The agent listens on two TCP ports:
| Port | What |
|---|---|
8787 | Plaintext HTTP + WebSocket. Always on (backwards compatibility with older apps). |
8788 | HTTPS + WSS with a self-signed certificate. On by default since agent 2.9.88; opt out with couchside tls off. |
There is no third machine. No relay, no broker, no push service, no account server. If the phone and the box aren't on the same network, nothing works. That is the design, not a limitation we plan to "fix" with a cloud tier.
Transport encryption
The agent mints its own RSA-2048 certificate on first start (via the box's openssl) and serves HTTPS beside plaintext. Because the certificate is self-signed, trust doesn't come from a CA. It comes from pinning at pairing time:
- The pairing QR rendered on the box's own screen carries the certificate's SHA-256 fingerprint. Reading it requires being physically in front of the box; that is the trust anchor.
- The app fetches the certificate, verifies its hash against the fingerprint from the QR, and pins the key. Every later connection compares the live certificate's RSA key against that pin and fails closed on mismatch: a box presenting a different key is treated as unreachable, never silently downgraded to plaintext.
- The key survives IP changes (re-signs reuse it), so the pin doesn't break when your router hands out a new lease.
One caveat: a box that was paired before TLS existed gets upgraded in place. The app sees the box advertise TLS on its status poll and pins the advertised fingerprint. That advert arrives over plaintext, so this upgrade path is trust-on-first-use: it defeats a passive eavesdropper, but an active man-in-the-middle present at the exact moment of upgrade could pin itself. Re-pairing by QR replaces the TOFU pin with the physical-presence one. New pairings always use the QR path.
Two flows still touch plaintext on a TLS box, deliberately and narrowly: Steam cover-art images and phone→box file uploads (the platform image loader and streamed uploader can't pin a self-signed certificate). Both authenticate with short-lived single-purpose tickets minted over the pinned channel; the bearer token itself never rides cleartext for a pinned box. Cover art is public imagery; the residual exposure is the bytes of files you upload.
Pairing and auth
Every state-changing route requires Authorization: Bearer <token>: a 48-hex-character (24-byte) random token minted on the box at install, stored at /etc/couchside/token (mode 600), compared in constant time. A phone gets it one of two ways, both requiring you to see the box's screen:
- QR. The box renders a pairing link as a QR code. The token rides the URL fragment (
#token=…), which browsers never send to a server: couchside.tv never sees it, and the relaunch page that opens the app runs entirely client-side. The page serving the QR on the box (/pair) answers only to loopback and validates theHostheader, so neither a LAN client nor a DNS-rebinding page in the box's own browser can read it remotely. - PIN. The app finds the box on the LAN and asks it to display a 6-digit PIN on its own screen. The PIN lives 120 seconds, allows 5 wrong attempts before the session burns, is rate-limited (new sessions every 3 s at most, on-screen pop at most every 5 s), and is exchanged for the token over one request. Worst-case brute force is roughly 1.7 guesses per second against a million combinations, which is about a week of continuously strobing a full-screen pairing page onto your TV. Loud, slow, and visible; but there is no per-IP lockout beyond this, which is why the LAN itself is the trust boundary (see the last section).
On the phone, the token is stored in the platform keychain (iOS Keychain / Android Keystore via expo-secure-store), per box.
Revocation
The agent has one token per box, not per device. Every paired phone holds the same secret. That makes revocation blunt by design:
couchside new-tokenmints a fresh token and restarts the agent. Every previously paired phone stops working immediately, and each one re-pairs by QR or PIN. There is no way to cut off one phone and keep the others; if that matters to you (shared house, revoked roommate), rotate and re-pair. Treat the token like an SSH key: anyone holding it controls the box (next section).
If couchside new-token or couchside tls reports an unknown command, your box has an older copy of the management CLI (both were added 2026-08-15). Plain couchside update will not fetch it when your agent is already current (that command compares agent versions and exits early), so reinstall explicitly:
couchside update --forceor, equivalently, curl -fsSL https://couchside.tv/install.sh | bash. The installer is idempotent: it keeps your existing token, config, and pairings.
What the token grants
Commands the agent runs are chosen from a frozen allowlist in the agent source: a client-supplied id is looked up, never interpolated, and subprocess is always an argv list, never a shell string. Root-side actions (reboot, session restart, journal reads) go through exact-path sudoers grants on fixed commands or argument-validating root-owned wrappers, never through wildcards. Riskier capabilities are off until you enable them on the box: couchside allow-updates, couchside allow-system-updates, couchside allow-launchers. The phone can never grant itself any of these.
But be clear about the baseline: the gamepad/remote path includes a virtual keyboard and mouse. A token holder can type into whatever session your box is showing; that's the product. The allowlist bounds what the HTTP API can execute directly; it is not a sandbox around someone who holds your token. This is why revocation and the pairing physical-presence requirements exist.
Discovery: what the box reveals to anyone
So the app can find boxes, the agent answers two unauthenticated probes from anything on your LAN:
GET /api/pingreturns{"couchside": true}plus agent version, the box's short hostname, the IP you reached it on, and (when TLS is up) the HTTPS port and certificate fingerprints.- A UDP broadcast responder on the same port number, with the same fields.
No token, no control, no user data, but it does tell any LAN scanner "a Couchside agent, version X, named Y, lives here." If that bothers you, it is the cost of one-tap discovery; adding a box by IP works with discovery firewalled off. The only other unauthenticated writes are the two PIN-pairing endpoints described above. Everything else returns 401 without the token, and the agent's own request log redacts query strings so tokens never land in the journal.
No telemetry, and exactly what does leave your LAN
No analytics, crash reporting, or advertising SDKs in the app; no third-party imports in the agent; no first-party collection endpoint exists at all; couchside.tv is static pages. We cannot see your usage because there is nothing to send it to.
"No telemetry" is not "never touches the internet." The complete list of outbound connections, all to well-known third parties, none carrying usage data:
| From | To | When / what |
|---|---|---|
| agent | github.com | Update check (compares versions, cached 6 h, only when the app asks) and downloading signed releases you install. The request's User-Agent carries the agent version. |
| app | store.steampowered.com | Game names, artwork, review scores for your library view. |
| app | itunes.apple.com / the app stores | App-update version check; purchases go through Apple/Google. |
Cover art for installed games is served by your box from Steam's local cache; the phone doesn't fetch it from a CDN.
What this does NOT protect against
- A compromised device already on your LAN, present at pairing time. The whole trust model bottoms out at "you can see the box's screen and your LAN is yours." An attacker with a LAN foothold can watch discovery, and if they're active during a TOFU auto-upgrade (or can strobe pairing PINs onto your TV for a week without you noticing), the model has failed. Couchside assumes a home LAN, not a hostile one.
- A lost or stolen phone, until you rotate. The token in its keychain keeps working until
couchside new-token. Nothing expires on its own. - Yourself, port-forwarding the agent. The agent binds all interfaces and its auth is one static bearer token with no lockout on the HTTP gate. Exposing 8787/8788 to the internet is categorically outside the design. Don't.
- Plaintext sniffing of un-upgraded pairs. A box on an agent older than 2.9.88, or an app that never picked up the TLS upgrade, still talks cleartext HTTP on 8787, including the bearer header. On your own WPA2/3 Wi-Fi that means other devices that already know your Wi-Fi password. Update both sides; check with
couchside tls status. - Uploaded file contents on a TLS box. The streamed uploader rides plaintext with a single-use ticket (the token stays off the wire, the bytes don't). Don't upload secrets you wouldn't broadcast on your LAN.
- Physical access to the box. The token is a file; anyone at the keyboard owns the machine anyway.
Verify it yourself
Every claim above is testable from any machine on your LAN:
curl http://YOUR-BOX.local:8787/api/pingThat is the discovery surface, exactly as described. Then confirm a credential-less client is refused:
curl -i http://YOUR-BOX.local:8787/api/statusThat returns 401. On the box, couchside tls status prints the live HTTPS port and certificate fingerprints; compare against what the handshake presents:
openssl s_client -connect YOUR-BOX.local:8788 </dev/null 2>/dev/null | openssl x509 -fingerprint -sha256 -nooutAnd the source of everything on this page: the agent (auth gate, pairing, discovery, TLS), the installer (token mint, sudoers grants, the CLI), and the app (pinned transport, keychain storage). Releases are signed with an offline Ed25519 key, and the installer this site serves is build-verified against those signatures. Found something? support@couchside.tv.